Moya: What's the best way to chain requests that are dependent on one another using RxMoya?

Created on 28 Oct 2016  Β·  28Comments  Β·  Source: Moya/Moya

I need to perform the following steps in order for my service to work:

  1. GET csrf_token
  2. Set csrf_token + jwt into http header
  3. Return the response

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!

question rxmoya

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!

All 28 comments

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:

  1. Checks if Endpoint/request is of POST method.
  2. if it is POST: GET a CSRF Token from server
  3. Injects token into request
  4. Complete Async Request.

Questions:

  1. My problem here is should there be a Provider within another Provider? The parent rxProvider has a csrfProvider embedded within the process. If not, how can I simplify this? Is there a better way of managing my providers?
  2. Do all providers need to end with .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!

NetworkingType.swift OAuthTokenRequest.swift

@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

  1. It lets me use the 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)
  2. It allows me to have a custom request function (gist) that reactively injects an OAuthToken to all endpoints that requiresAuth from ProxyAPIType

Let 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

  1. I call Networking.request for my .me endpoint
  2. Networking.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)
  3. 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.
  4. Looking at 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 Alamofire

Let 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?

Was this page helpful?
0 / 5 - 0 ratings