Alamofire 5: Advanced Usage Topics?

I'm in the process of rewriting Alamofire's AdvancedUsage documentation and thought I'd reach out to see if there were any topics the community would like to see covered, aside from updating the current topics for Alamofire 5. I plan to cover all of the APIs not covered by the Usage documentation, but I'm also going to include some guidance on integrating Alamofire in general.

So, is there anything anyone would like to see?

2 Likes

Hello!

I would like to see "request retrying" covered in depth.
As I understand it, it has now been moved to "RequestInterceptor".

I have managed to get something basic working (by pusseling togheter some posts from github Issues and Stack Overflow), but my main problem is a more advanced usage scenario, I need the Alamofire 5 equivalent to the old:

private var requestsToRetry: [RequestRetryCompletion] =

The problem I have is that when my app starts, it fires off ~10 request to a api which requires auth.
This in turn fires of 10 failures with 10 calls to my RequestInterceptors "retry" function which makes my app do the auth 10 times.

I need a good example of a <rock solid production ready "RequestInterceptor"> that can queue requests if an auth is already in progress..

Thanks!

1 Like

It might be helpful to include a discussion of background transfers. I understand that AF 5 won't have explicit support for this but some direction might be helpful for those using that feature.

+1 to this. One of the more difficult customizations that I have done with Alamofire 5 is a re-authorization retrier. What I mean is this behavior:

  1. Assume at any given time that there are multiple in-flight requests that contain an Authorization header which contains an OAuth token.
  2. Eventually that token expires so at some point one of those in-flight requests fails with 401.
  3. When the first one failed, I used that as a signal to kick off a request using stored credentials to the OAuth endpoint to request a new token.
  4. While the new-token-request is in flight, the other requests may be failing with 401. Those requests are saved into a retry list.
  5. Once the new-token request succeeds, then we set the new auth token in the RequestRetrier and retry the failed 401 requests.

Thanks!

1 Like

Rather than a highly complex example in documentation not everyone reads, and to discourage copy-pasting untested code into project, it seems like the best idea would be to include a RequestRetrier with Alamofire that tracks ongoing requests for you. This would allow:

  1. Full documentation for that particular use case.
  2. Extensive test coverage, as the examples in docs are not tested.
  3. Required updating as Alamofire undergoes changes, ensuring they always work and don't go stale as examples.
  4. Optimized integration into Alamofire, whether for performance or other aspects.

So, while I'm rewriting the advanced documentation right now, it will not include an extensive example of this case. In fact, I'm removing most of the full code examples and just providing excerpts to ensure users aren't copying untested and unmaintained code into their projects. In the future I think anything that's worthy of such extensive documentation is likely worth just including in the library.

Anyone interested in this feature is encouraged to submit a PR implementing an initial version.

1 Like

@ehyche this is actually quite a brilliant solution. It's very relevant for me, for I need to maintain an accessToken from Firebase to call my server. However, the current technique isn't very robust, but you eloquently put here a basic architecture to get a new token re-issued for in-flight requests, and keep the whole system going.

The alternative solution is:

  1. Create an asynchronous Alamofire RequestAdapter that uses the current authenticated user to user.getIDToken {}
  2. Wait for getIDToken completion with either the token or error
  3. Modify the urlRequest, by adding the authorization header with bearerToken

Possibly BAD Example:

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void)
    {
        guard let user = self.currentUser
        else {
            completion(.failure(MyServerUnauthorizedError()))
            return
        }
        
        user.getIDToken { (accessToken, error) in
            if let error = error {
                completion(.failure(error))
            } else if let accessToken = accessToken {
                var urlRequest = urlRequest
                urlRequest.headers.add(.authorization(bearerToken: accessToken))
                completion(.success(urlRequest))
            } else {
                completion(.failure(MyServerWeirdError()))
            }
        }
    }

Issues with the above approach are with unauthorized state handling, a possible weird error case, and possible failure mid-flight like you described.

I think my above code is people's first attempt at solving it, but miss out on the re-authorization retry for mid-flight failures. So they end up for settling with a less robust solution thinking that's the end of it.

I would very much like to see a working re-authorization retrier example that can help establish better practices.

@ehyche and @otri you are definitely on to the solution that we've built internally in our own Nike networking library on top of AF. Now that we've made adapt async, you can now refresh expired (or soon to expire creds) prior to executing the request. This solves "most" use cases. However, some systems reserve the ability to revoke those creds server-side at any time meaning the expiration info of your creds may not be correct according to the server. This results in the 401 case @ehyche is alluding to.

It gets even more complicated when you consider an authorization router in front of some microservices (very common). It's really important to understand exactly what 401 means to all the service layers you're interacting with. For example, we have some cases where the bearer token passed in the Authorization header (OAuth2) is valid, passes through the auth router, and hits the microservice. However, the microservice team may be a social API that allows you to see someone else's profile. If you aren't already friends, that microservice may return a 401 since you are "not authorized". This can put you into an endless refresh loop if you are only keying off the 401 status code. We've had to add additional info OAuth2 header info to all our auth routers to allow the clients (our networking library wrapping AF) to know whether the 401 came from the auth router or microservice.

Anyways, I'm with @Jon_Shier on this and think we (the ASF team) should provide an actual AF type that handles all of the above logic for you. Examples are helpful to understand the problem, but will just be copy/pasted in cases where they absolutely shouldn't be. What I think we need is an interceptor that can handle all the cases I specified above letting you plug in the credential, how to set it on the URLRequest, and how to call refresh. Then this type will handle all the other complexity of queuing all the requests while refresh is occuring, whether that be from the client making additional requests, or in-flight requests are coming back with a 401. All of those would be queued up, and retried once a successful refresh occurred. If refresh was unsuccessful, all queued requests would have their completions called with an authorization failure. There's also quite a bit of complexity around thread safety since the interceptor can be called from a bunch of different threads all at the same time. Especially during app start up when you have an expired access token and fire a few dozen requests to try and get all your state updated. You also need to be very careful how you call the completions to hop out of all the locks so you don't accidentally create a re-entrant deadlock.

The final tricky piece is what should be considered an "unrecoverable" failure during refresh. This will be a bit different depending on what auth server you are hitting, but generally it would be that you actually hit the server, and received some status code back that means your refresh token is never going to work anymore and you need to re-login. We'll probably build a default there allowing clients to customize that functionality as well since they may need to.

Anyways, this is something that I'd love to get built into AF 5.2. If there's anything else you'd like to see in this type of interceptor that's not listed above, please feel free to chime in before the development gets kicked off. I'd like to have something put together for this sometime this month.

Cheers.