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