I need to perform the following steps in order for my service to work:
Is there a specific way where my endpoint enclosure is able to do a GET request using the same provider? Or would I need to invoke another provider within the endpoint enclosure to invoke another request to do my GET request?
let endpointClosure = { (target: GameAppMoyaAPI) -> Endpoint<GameAppMoyaAPI> in
let endpoint: Endpoint<GameAppMoyaAPI> = Endpoint<GameAppMoyaAPI>(
URL: target.baseURL.URLByAppendingPathComponent(target.path).absoluteString,
sampleResponseClosure: {.NetworkResponse(200, target.sampleData)},
method: target.method,
parameters: target.parameters,
parameterEncoding: target.encoding
)
switch target {
case .AuthenticateUser:
return endpoint
default:
// 1. Check if token is expired from Locksmith
// 2. If expired, GET new access_token with refresh_token
// 3. Write new access_token back into Locksmith
// 4. Else, add token from Locksmith to Header
let dict = Locksmith.loadDataForUserAccount()
// FIXME: This is temporarily in
let access_token = dict!["jwt_token"] as! String
// Need to add csrf_token to header for POST
if target.method == .POST{
// Would I be invoking another provider here?
let csrf = ???
return endpoint.endpointByAddingHTTPHeaderFields(
[
"Authorization": "Bearer \(access_token)",
"X-CSRFToken": csrf
])
}
return endpoint.endpointByAddingHTTPHeaderFields(["Authorization": "Bearer \(access_token)"])
}
}
let provider = RxMoyaProvider<MyMoyaAPI>(endpointClosure:endpointClosure)
provider.request(.PostRequest(parameter:some_parameter))
.subscribe{
...
}
Thanks!
Update:
I created the following way to get my app to work, but do not know if this is best practice or proper design. Would love suggestions and feedback
// moyaapi.swift
public enum MoyaAPI {
case GetCSRF()
case AuthenticateUser(email:String, password:String) //
case RefreshAccessToken(expiredToken: String, refreshToken:String)
}
etc ...
// customAPIClosure.swift
class MoyaAPIClosures{
static let requestClosure = { (endpoint: Endpoint<MyAppMoyaAPI>, done: MoyaProvider.RequestResultClosure) in
var request: NSMutableURLRequest = endpoint.urlRequest.mutableCopy() as! NSMutableURLRequest
var thisEndpoint: Endpoint = endpoint
// Do CSRF HERE
print("Request Closure Config")
if thisEndpoint.method == .POST{
let csrfProvider = RxMoyaProvider<MyAppMoyaAPI>(endpointClosure: MoyaAPIClosures.endpointClosure)
print("ENDPOINT CLOSURE CONFIG")
csrfProvider
.request(MyAppMoyaAPI.GetCSRF())
.filterSuccessfulStatusCodes()
.mapObjectOptional(CSRFToken.self)
.subscribe{ e in
switch e {
case .Next(let token):
// Set HTTP Headers
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(token?.csrf_token, forHTTPHeaderField: "X-CSRFToken")
request.HTTPShouldHandleCookies = true
// When using RxSwift returns must be done in closures
// The authProvider can make its own network calls to sign your request.
// However, you *must* call `done()` with the signed so that Moya can
// actually send it!
done(.Success(request))
case .Error(let error):
print(error)
default:
break
}
}
// DO NOT ADD disposeBag here ... request is not finished yet.
}else{
done(.Success(request))
}
}
static let endpointClosure = { (target: MyAppMoyaAPI) -> Endpoint<MyAppMoyaAPI> in
print("Endpoint Closure Config")
let endpoint: Endpoint<MyAppMoyaAPI> = Endpoint<MyAppMoyaAPI>(
URL: target.baseURL.URLByAppendingPathComponent(target.path).absoluteString,
sampleResponseClosure: {.NetworkResponse(200, target.sampleData)},
method: target.method,
parameters: target.parameters,
parameterEncoding: target.encoding
)
switch target {
case .AuthenticateUser:
// Authentication does not require httpheaders
return endpoint
default:
// All authorized GET, POST requests require JWT
let user = User()
let userName = user.username
let dict = Locksmith.loadDataForUserAccount(userName)
let access_token = dict!["jwt_token"] as! String
return endpoint.endpointByAddingHTTPHeaderFields(["Authorization": "Bearer \(access_token)"])
}
}
}
// customvc.swift
class CustomTableViewController: UITableViewController {
let rxProvider = RxMoyaProvider<MyAppMoyaAPI>(endpointClosure:MoyaAPIClosures.endpointClosure, requestClosure: MoyaAPIClosures.requestClosure, plugins:[NetworkLoggerPlugin(verbose:true)])
let disposeBag = DisposeBag()
func updateGameData(){
var listOfUpdates: [Dictionary<String,AnyObject>] = []
// Explicit Data Updates
for indexOfRowChanged in rowsChanged{
let obj = DataArray[indexOfRowChanged]
let quickDict:[String:AnyObject] = [
"game_id": obj.gameID,
]
listOfUpdates.append(quickDict)
}
// Return if no updates were made
if listOfUpdates.isEmpty{
return
}
rxProvider
.request(MyAppMoyaAPI.GameData(
UUID: aID!,
updates: listOfUpdates)
)
.filterSuccessfulStatusCodes()
.subscribe({ event -> Void in
switch event{
case .Next(let results):
print(results)
print("FINISH POST")
case .Error(let _):
print("ERROR!!!")
default:
break
}
}).addDisposableTo(disposeBag)
}
}
So how this works is... When the Request/Endpoint is being created:
Questions:
.addDisposable(disposeBag)? In my case, If i did do this with csrfProvider, it would have disposed the request and it would not have gone through to server. Thanks!
You definitely do need to add your request to a disposeBag, start with that.
On issue 2: I'm going to open source my networking stack so you can see how I inlined my token request into the provider. You don't need to create a second provider for that
@rlam3 I changed my mind on the open-sourcing of the networking stack, decided not to make a repo for 2 files of networking code.
Basically, I have a class that wraps my provider called Networking, it looks like
struct Networking: NetworkingType {
typealias T = ProxyAPI
let provider: OnlineProvider<ProxyAPI>
}
and Networking's request function looks like
func request(_ token: ProxyAPI) -> Observable<Response> {
let actualRequest = self.provider
.request(token)
.filterSuccessfulStatusCodes()
.catchError(parseMoyaError)
if token.requiresAuth {
// makes sure the oauthtoken is fresh
return OAuthTokenRequest().flatMap { _ in actualRequest }
} else {
return actualRequest
}
}
everything uses this request function. except the OAuthTokenRequest(), which is a function that goes through the provider's request function
func OAuthTokenRequest() -> Observable<AuthToken> {
guard let authToken = AuthToken.local() else {
return .error(Error(Strings.Error.NotLoggedIn.title,
description: Strings.Error.NotLoggedIn.description))
}
if authToken.isValid {
return .just(authToken)
}
return request(.refreshAuth(refreshToken: authToken.refreshToken))
.mapJSON().map(AuthToken.decodeValue)
.do(onNext: { $0.save() })
}
Let me know if you have any other questions π
Moya really affords itself to solving this problem
@AndrewSB Thanks for the reply.
I understand you wrap your provider in a Networking class/struct. What is NetworkingType though? I'm not understanding what you're subclassing here...
Where would my requestClosure and endpointClosure be? Also in the provider? I need to do be able to modify my request based on its HTTP methods.
Where does Networking's request function exist? Is it in the Networking class/struct?
If it exists within the same Networking struct/class... what is the _ in front of token? And
What exactly are you trying to do here:
let actualRequest = self.provider
.request(token)
.filterSuccessfulStatusCodes()
.catchError(parseMoyaError)
You're calling back on the same provider class and passing a token into the request? Or is it the API enum?
Does OAuthTokenRequest also exist inside of Networking class/structure, or in it's own seperate class? Also, what are the trade offs of using an Observable rather than Endpoints?
If you can't provide the opensource version of it. Could you please provide me with a few gist? Thanks!
You're welcome π
I understand you wrap your provider in a Networking class/struct. What is NetworkingType though? I'm not understanding what you're subclassing here...
NetworkingType is
protocol NetworkingType {
associatedtype T: TargetType, ProxyAPIType
var provider: OnlineProvider<T> { get }
}
its a wrapper for the provider (I'm using a custom provider that takes into account the online status, OnlineProvider is just a subclass of RxMoyaProvider), that has the custom request function I was talking about.
Where would my requestClosure and endpointClosure be? Also in the provider? I need to do be able to modify my request based on its HTTP methods.
The requestClosure and endpointClosure stay inside the provider.
Where does Networking's request function exist? Is it in the Networking class/struct?
If it exists within the same Networking struct/class... what is the _ in front of token?
Yup π Networking's request function is inside the Networking struct.
The _ in request(_ token: ProxyAPI)? That just makes it so you can call request(.getMyTweets) instead of request(token: .getMyTweets). It's a swift language feature, just syntactic sugar π
What exactly are you trying to do here:
You're calling back on the same provider class and passing a token into the request? Or is it the API enum?
You're right again, actualRequest calls the request function on the provider, it passes in the OAuthToken in the flatMap right after the definition.
Does OAuthTokenRequest also exist inside of Networking class/structure, or in it's own seperate class?
OAuthTokenRequest is a global free function.
what are the trade offs of using an Observable rather than Endpoints?
Not sure what you mean π If you rephrase that, I'm happy to help you understand it further π
If you can't provide the opensource version of it. Could you please provide me with a few gist? Thanks!
@AndrewSB I think my question should be... Did you wrap your provider within networking struct?
I'm trying to understand what is going on here with your protocol
protocol NetworkingType {
associatedtype T: TargetType, ProxyAPIType
var provider: OnlineProvider<T> { get }
}
struct Networking: NetworkingType {
typealias T = ProxyAPI
let provider: OnlineProvider<ProxyAPI>
}
Where does TargetType and ProxyAPIType come from?
My ultimate goal is to move from MoyaProvider to RxMoyaProvider for all my queries to the backend server if possible. In order for me to do this what would be the best way to structure my wrapper around my provider to be used? Thanks!
Really appreciate your help!
I am indeed wrapping my provider inside this Networking struct. I later create an instance of my networking client by
static func newDefaultNetworking() -> Networking {
return Networking(provider: OnlineProvider(endpointClosure: endpointsClosure))
}
TargetType comes down from Moya, ProxyAPIType (Proxy is the name of the iOS app from which I'm showing you code) is a protocol that defines some additional params that each of my endpoints must provide:
protocol ProxyAPIType {
var parameterEncoding: ParameterEncoding { get }
var accept: String { get }
var contentType: String { get }
var contentLength: Int? { get }
var requiresAuth: Bool { get }
}
So for me, having this Networking wrapper accomplishes 2 main things
OnlineProvider to only send networking requests when Reachability says I have internet connection (watch out for #722, I haven't solved the problem there yet)request function (gist) that reactively injects an OAuthToken to all endpoints that requiresAuth from ProxyAPITypeLet me know if you have any other questions, I'm here to help π
@AndrewSB Thanks! And is this code valid for swift 3 because i think they deprecated typealias for associatedtype?
I'm not sure if your OAuthtokens have expirations. But how have you been handling the expirations of your tokens and retrieve a new one using the same wrapper provider? ... I'm using a singleton keychain to hold onto my tokens. I'm thinking of moving my token validation into my requestClosure to validate expiration of token and retrieve a new one from there with the same RxProvider. But I'm not sure how to structure my provider in such a way that I am able to do it. Was wondering if you had any input on this issue or ran into it yourself. Thanks!
Reference:
https://www.natashatherobot.com/swift-protocols-with-associated-types/
Associated types work, I'm using this in swift 3, I can show you how I'm doing my OAuthToken expiry, I'll post a gist soon
EDIT: OAuthTokenRequest gist
So in my Networking struct's request function, if the target required authentication, I flatMap this OAuthTokenRequest into the target, to make sure it's valid and fresh, then I grab it from my persistent storage
Closing this for now. Let us know if you still have any questions, @rlam3 :)
@AndrewSB Would love to see how your AuthToken.swift looks like. I'm a bit puzzled on how you were able to chain this request... I'm beginning to understand a little bit more about how you are chaining actual request to the authorized request.... Thanks! Really appreciate your help!
Also, could you also give me a gist of what you are doing for the .error method you are calling? It seems to me it should have been returning a nil or an empty string... right?
My AuthToken.swift just holds a model struct, nothing interesting there.
Let me try to illustrate the sequence in which my requests chain through an example
Networking.request for my .me endpointNetworking.request goes ahead and adds some decoration to the response, i.e. filtering out successful status codes, and parsing the error if there was one (https://gist.github.com/AndrewSB/4f1f1256a0a35e82ac44cf7d3dba56f0#file-networkingtype-swift-L15)Networking.request also signs the request with an AuthToken if required https://gist.github.com/AndrewSB/4f1f1256a0a35e82ac44cf7d3dba56f0#file-networkingtype-swift-L20. Since this is a flatMap, it passes control to the OAuthTokenRequest, and only sends the .me request after OAuthTokenRequest() .nexts.OAuthTokenRequest (https://gist.github.com/AndrewSB/973d81843a834c68c8cfb916830cd92d), you can see that it either returns a local valid token (in which case the request is signed and it goes out to Alamofire), or does a Networking.request to the .refreshAuth endpoint, which would repeat steps 1-2, and skip over 3 & 4, since the .refreshAuth doesn't requireAuth. Once we hear back from the network with a new AuthToken, our OAuthTokenRequest() returns the AuthToken, we sign the .me request, and it goes out to AlamofireLet me know if I was unclear anywhere, or if you have further questions!
Re the .error: which error are you referring to? This one?
@AndrewSB Thanks man! I have a situation which would require me to chain the requests of two observables.. It is similar to what you already did....
Example: Say I have OAuthTokenRequest1 and OAuthTokenRequest2 that I have to chain together in the final request of Networking... how would you go about mapping these?
I saw this was part of your code.
return OAuthTokenRequest().flatMap { _ in actualRequest }
But at the same time I would like to have a boolean determine when to chain and when not to chain
if requiresAuth1 and requiresAuth2{
// Chain auth1 and auth2
}else if requiresAuth 1{
return auth1().flatmap{_ in actualrequest}
}else if requiresAuth2{
return auth2().flatmap{_ in actualrequest}
}
Thanks!
can you make your example more concrete? Are you saying your implementation requires two tokens to sign each request? OAuth and something else?
@AndrewSB, Lets say only post/put requests request requires a csrf token and a access token and on get requests it only requires the auth token
One of the request is to obtain a fresh crsf and the second is to validate the existing access token and then if it doesn't it has to obtain a new one as well and set it back into keychain prior to firing off the actual request.
Apologies for the confusion. Let me know if you need more info
My way of thinking of this was to do seperate csrfrequest and oauthrequest and then chain them together using boolean set on api
Thanks!
I'd add a requiresCSRF and a requiresOAuth to each your TargetType enum, and then do some sort of pattern matching, maybe:
switch (target.requiresCSRF, target.requiresOAuth) {
case (false, false): return actualRequest
case (true, false): return CSRFTokenRequest().flatMap { _ in actualRequest }
case (false, true): return OAuthTokenRequest().flatMap { _ in actualRequest }
case (true, true): return Observable.zip([CSRFTokenRequest(), OAuthTokenRequest()]) { _ in actualRequest }
}
So yup, you were on the right track!
@AndrewSB Thanks for the tip! I got the following error after trying to use the observable.zip design pattern....
Cannot convert value of type '(_) -> Observable<Response>' to expected argument type '([_]) -> _'
is there something wrong with my function request that was wrapping the switch case?
func request(_ token: MidoriMoyaAPI) -> Observable<Moya.Response> {
/// switch case....
}
Thanks!
Sorry about that @rlam3! I forgot that Observable.zip(Array<T>) expected the array to be an array of one element. Try this instead
Observable.zip(CSRFTokenRequest(), OAuthTokenRequest()) { _, _ in actualRequest }
@AndrewSB
I tried this and it resolved... the problem... but I don't know if this is the proper way to sequentially do this....
return CSRFTokenRequest().flatMap(){ _ in
self.OAuthTokenRequest().flatMap{ _ in actualRequest}
}
On another note,
I'm getting the following error using your method...:
Cannot convert value of type '(_, _) -> Observable<Response>' to expected argument type '(_, _) -> _'
@AndrewSB If OAuthTokenRequest is dependent on CSRFTokenRequest. Would zip still work? Or should it be be my proposed way of chain blocking? Would love your feedback on this. Thanks!
Hey @rlam3! Sorry about not responding, this must have slipped past me on my notifications!
If your OAuthTokenRequest depends on your CSRFTokenRequest then you don't want to zip, Zip is good when you have two operations that don't depend on each other you'd like to have completed, (diagram for reference) http://rxmarbles.com/#zip
If your OAuthTokenRequest is dependent on CSRFTokenRequest, you should
CSRFTokenRequest()
.flatMap { csrfToken in OAuthTokenRequest(csrfToken) }
.flatMap { _ in actualRequest }
@rlam3 I renamed this issue to reflect what the discussion turned into, I hope that's alright with you, please let me know if I should change it back or change it to something else π
Just wanted to thanks @AndrewSB for detailed explanation and @rlam3 for all the questions I wanted to ask. π
Reading above discussion gave me more understanding on how to use Moya in real world scenarios. Thanks again!
@AndrewSB I face a problem when the access token expires and multiple requests happen, that all request a new access token via the refresh token, at the same time.
I noticed that Ello-iOS is handling multiple unauthorized requests by call request/refresh token at a time and waiting for a new token on other requests.
I donβt see the similar feature on Artsy Eidolon.
Does trackInFlights will help. I didnβt see much information or document about it, just read in some related issues/commits?
I'd say (after thinking for about 5 seconds) make your authtoken observable
share replays, that way you'll only have one AuthToken request actually
issued.
You'll also have to handle invalidation of the token somewhere internally,
as soon as the token expires the replay-able event should be dropped so the
next subscription to the observable triggers an actual auth token refresh
While working on this myself, I used http://github.com/Expirable and
thinking of my AuthTokens in that way really helped me
Here if you need clarification!
On Tue, May 23, 2017 at 12:22 AM (Alfred) notifications@github.com wrote:
@AndrewSB https://github.com/andrewsb I face a problem when the access
token expires and multiple requests happen, that all request a new access
token via the refresh token, at the same time.I noticed that Ello-iOS is handling multiple unauthorized requests by
call request/refresh token at a time and waiting for a new token on other
requests.I donβt see the similar feature on Artsy Eidolon. Does your code support
this?Does trackInFlights will help. I didnβt see much information or document
about it, just read in some related issues/commits?β
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Moya/Moya/issues/748#issuecomment-303312259, or mute
the thread
https://github.com/notifications/unsubscribe-auth/ADo1dFHJvTTo97u-Jt-W6a9DvlzVhNJvks5r8okGgaJpZM4KjFdG
.
@AndrewSB think you meant https://github.com/AndrewSB/Expirable :wink:
I did, thanks for catching that @pedrovereza!
Also, this thread is becoming awfully long, I'm going to lock it so it doesn't become much longer. @dangthaison91 if you want to follow up on handling multiple unauthorized requests at once, can you create a new issue and mention your earlier comment in it?
Most helpful comment
Just wanted to thanks @AndrewSB for detailed explanation and @rlam3 for all the questions I wanted to ask. π
Reading above discussion gave me more understanding on how to use Moya in real world scenarios. Thanks again!