4
votes

I'm re-writing the networking in my app with alamofire 5 (rc3) and I'm trying to retry the request if it fails due to my JWT token being expired, I can get this to work if I simply tag a .validate() onto the request meaning the API 401 response causes the request to 'fail' and then be passed to the RequestRetrier, however every other 400-499 request my API returns data in the same form and the message is useful but by using .validate() it throws away the useful object-ifying that .decodeResponse() gives:

{
    "data": null,
    "message": "Token has expired",
    "status": "error"
    /* has 401 error code */
}
class NetworkInterceptor: RequestInterceptor {

    // MARK: - RequestAdapter
    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        print("adapting")
        var adaptedRequest = urlRequest
        let token = NetworkService.sharedInstance.authToken
        adaptedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        completion(.success(adaptedRequest))
    }


    // MARK: - RequestRetrier
    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        if let response = request.task?.response as? HTTPURLResponse, let WWWheader = response.allHeaderFields["Www-Authenticate"] as? String, response.statusCode == 401, WWWheader == "Bearer realm=\"everywhere\"" {
            print("Refreshing token for retry...")
            NetworkService.sharedInstance.refreshTokens { (success, _, _) in
                print("Refreshed token, retrying request")
                completion(.retry)
            }
        } else {
            completion(.doNotRetry)
        }
    }

}

An example function to call my API inside my network manager looks like as follows, session is just the normal session with the Network interceptor attached (and working).

A typical API call function looks like this:

func sendMove(id: Int, move: Move, completion: @escaping APICompletionHandler<GameRender>) {
    session.request(APIRouter.sendMove(id: id, move: move)).responseDecodable { (response: DataResponse<APIResponse<GameRender>, AFError>) in
        switch response.result {
        case .success(let apiResponse):
            if apiResponse.status == .success {
                // Data from API and success
                completion(true, apiResponse.data, apiResponse.message)
            } else {
                // Data from API but not success
                completion(false, apiResponse.data, apiResponse.message)
            }
        case .failure(let data):
            // Could not get anything from API
            completion(false, nil, data.localizedDescription)
        }
    }
}

You can see I return some form of error response inside case .success(let apiResponse) if the body's "success" key is false. This however means the request is never put to the requestRetrier

If however I use .validate()

func sendMove(id: Int, move: Move, completion: @escaping APICompletionHandler<GameRender>) {
    session.request(APIRouter.sendMove(id: id, move: move)).validate().responseDecodable { (response: DataResponse<APIResponse<GameRender>, AFError>) in
        switch response.result {
        case .success(let apiResponse):
            if apiResponse.status == .success {
                // Data from API and success
                completion(true, apiResponse.data, apiResponse.message)
            } else {
                // Data from API but not success
                // NOW THIS NEVER RUNS
                completion(false, apiResponse.data, apiResponse.message)
            }
        case .failure(let data):
            // Could not get anything from API
            completion(false, nil, data.localizedDescription)
        }
    }
}

You can now see that the else{} in the first portion of the switch never gets run. These two patterns seem to be at odds, is there a way of calling retry on a specific call, after parsing, for example if (token needs refreshing?) -> retry this request

1
You want to trigger retry and keep the failed response's data in your completion handler? Why would you want to do that?Jon Shier
I only need to retry on a 401 when the token has expireduser5650823
Ah, well the validate call turns all 400+ status codes into errors, so you'll need to customize it if you only want an initial failure for 401s or 401s with certain headers. Otherwise everything with that range of codes will come across as a .failure result.Jon Shier
Is there another pattern that’s more common for accepting the data object converted in the failure portion?user5650823
I mean you can get the behavior you want by making your validate call only catch the 401 you want, allowing all other responses to pass through to the response handler normally. That is, if I'm understanding what you want correctly.Jon Shier

1 Answers

3
votes

Fundamentally, to trigger retry, some step along Alamofire's request path must throw an error. Response handlers, like responseDecodable, will only parse the response data if no errors were produced during the request. Using validate() produces an error for all invalid response codes and response Content-Types. Your simplest option here is to customize validate() using a passed closure to only produce an error for the situations you want to trigger retry. Then your response handlers will always parse their data and you can treat other failures however you want.

A more advanced solution would be to write your own ResponseSerializer which parses the response data when there are certain errors, but not all.