Chaining API calls - ios

I've built myself an APIService class, handling all interaction with APIs my App is using
func callEndpointX(token: String, completion: #escaping(Result<User, APIError>) -> Void) {
guard let endpoint = URL(string: apiBaseUrl + "/endpointX") else {fatalError()}
var request = URLRequest(url: endpoint)
request.addValue("Bearer " + token, forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let jsonData = data
else { ... completion(.failure(.responseError)); return }
do {
let response = try JSONDecoder()...
completion(.success(response.detailresponse!))
} catch {
...
completion(.failure(.decodingError))
}
}
dataTask.resume()
}
My challenge is that the Bearer Tokens I'm passing along do expire. What I would like to do in case of an expired token is to silently refresh the token for the user before hitting the api.
So I've got a mini function func tokenExpired() -> Bool {...} checking if the Token has alreaady expired and I have access to another API all refreshToken in a similar way as func callEndpointX above.
What I'm struggling with is how to chain these together to avoid race conditions - i.e. when func callEndpointX gets called, it should check func tokenExpired() and not continue any other work until that's done. If expired, it should first execute refreshToken and only after that returns continue with finally executing the ret of func callEndpointX.
All this chaining is messing with my brain and I was hoping someone would be able to guide me here.
Many thanks!!!

A solution is to call recursively callEndPointX after token refresh is completed.
Assume your token request is like this:
func requestToken(completion:#escaping((String) -> Void))
then
func callEndpointX(token: String, completion: #escaping(Result<User, APIError>) -> Void) {
if tokenExpired() {
requestToken(completion: { newToken in
callEndpointX(token: newToken, completion: completion)
})
}
else {
// your code for callendpointX
}
}

Related

How to define a fallback case if a remote GET request fails?

I recently started with iOS development, and I'm currently working on adding new functionality to an existing app. For this feature I need to obtain a JSON file from a web server. However, if the server is unreachable (no internet/server unavailable/etc), a local JSON needs to be used instead.
In my current implementation I tried using a do catch block, but if there's no internet connection, the app just hangs instead of going to the catch block. JSON parsing and local data reading seem to work fine, the problem is likely in the GET method, as I tried to define a callback to return the JSON data as a separate variable, but I'm not sure if that's the correct way.
What is the best way to handle this scenario?
let url = URL(string: "https://jsontestlocation.com") // test JSON
do {
// make a get request, get the result as a callback
let _: () = getRemoteJson(requestUrl: url!, requestType: "GET") {
remoteJson in
performOnMainThread {
self.delegate.value?.didReceiveJson(.success(self.parseJson(jsonData: remoteJson!)!))
}
}
}
catch {
let localFile = readLocalFile(forName: "local_json_file")
let localJson = parseJson(jsonData: localFile!)
if let localJson = localJson {
self.delegate.value?.didReceiveJson(.success(localJson))
}
}
getRemoteJson() implementation:
private func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
// Method which returns a JSON questionnaire from a remote API
var request = URLRequest(url: requestUrl) // create the request
request.httpMethod = requestType
// make the request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// check if there is any error
if let error = error {
print("GET request error: \(error)")
}
// print the HTTP response
if let response = response as? HTTPURLResponse {
print("GET request status code: \(response.statusCode)")
}
guard let data = data else {return} // return nil if no data
completion(data) // return
}
task.resume() // resumes the task, if suspended
}
parseJson() implementation:
private func parseJson(jsonData: Data) -> JsonType? {
// Method definition
do {
let decodedData = try JSONDecoder().decode(JsonType.self, from: jsonData)
return decodedData
} catch {
print(error)
}
return nil
}
If you don't have to use complex logic with reachability, error handling, request retry etc. just return nil in your completion in case of data task, HTTP and No data errors:
func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
var request = URLRequest(url: requestUrl)
request.httpMethod = requestType
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Task error
guard error == nil else {
print("GET request error: \(error!)")
completion(nil)
return
}
// HTTP error
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("GET request failed: \(response!.description)")
completion(nil)
return
}
// No data
guard let data = data else {
completion(nil)
return
}
completion(data)
}
task.resume()
}
let url = URL(string: "https://jsontestlocation.com")!
getRemoteJson(requestUrl: url, requestType: "GET") { remoteJson in
if let json = remoteJson {
print(json)
...
}
else {
print("Request failed")
...
}
}
func NetworkCheck() -> Bool {
var isReachable = false
let reachability = Reachability()
print(reachability.status)
if reachability.isOnline {
isReachable = true
// True, when on wifi or on cellular network.
}
else
{
// "Sorry! Internet Connection appears to be offline
}
return isReachable
}
Call NetworkCheck() before your API request. If It returns false, read your local json file. if true do remote API call.
Incase after remote API call, any failure check with HTTP header response code.
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
}
I think you need to stop the request from hanging when it’s waiting for a response. The app might be running on a poor connection and be able to get some but not all the data in which case you likely want to failover to the local JSON.
I think you can roughly use what you have but add a timeout configuration on the URLSession as described here: https://stackoverflow.com/a/23428960/312910

Refresh access token with URLSession after getting a 401 response code & retry request

I'm working on building a networking client for my iOS application which uses OAuth 2.0 Authorization techniques (Access & Refresh Token). There is a feature for my networking client that I have been struggling to implement:
When a 401 error occurs that means the Access Token has expired and I need to send a Refresh Token over to my server to obtain a new Access Token.
After getting a new Access Token I need to redo the previous request that got the 401 error.
So far I have written this code for my networking client:
typealias NetworkCompletion = Result<(Data, URLResponse), FRNetworkingError>
/// I am using a custom result type to support just an Error and not a Type object for success
enum NetworkResponseResult<Error> {
case success
case failure(Error)
}
class FRNetworking: FRNetworkingProtocol {
fileprivate func handleNetworkResponse(_ response: HTTPURLResponse) -> NetworkResponseResult<Error> {
switch response.statusCode {
case 200...299: return .success
case 401: return .failure(FRNetworkingError.invalidAuthToken)
case 403: return .failure(FRNetworkingError.forbidden)
case 404...500: return .failure(FRNetworkingError.authenticationError)
case 501...599: return .failure(FRNetworkingError.badRequest)
default: return .failure(FRNetworkingError.requestFailed)
}
}
func request(using session: URLSession = URLSession.shared, _ endpoint: Endpoint, completion: #escaping(NetworkCompletion) -> Void) {
do {
try session.dataTask(with: endpoint.request(), completionHandler: { (data, response, error) in
if let error = error {
print("Unable to request data \(error)")
// Invoke completion for error
completion(.failure(.unknownError))
} else if let data = data, let response = response {
// Passing Data and Response into completion for parsing in ViewModels
completion(.success((data, response)))
}
}).resume()
} catch {
print("Failed to execute request", error)
completion(.failure(.requestFailed))
}
}
}
Endpoint is just a struct that builds a URLRequest:
struct Endpoint {
let path: String
let method: HTTPMethod
let parameters: Parameters?
let queryItems: [URLQueryItem]?
let requiresAuthentication: Bool
var url: URL? {
var components = URLComponents()
components.scheme = "http"
components.host = "127.0.0.1"
components.port = 8000
components.path = "/api\(path)"
components.queryItems = queryItems
return components.url
}
func request() throws -> URLRequest {
/// Creates a request based on the variables per struct
}
}
Where do I put the code that allows the FRNetworking.request() to get a new token and retry the request?
I have done the following inside the else if let data = data, let response = response statement:
if let response = response as? HTTPURLResponse {
let result = self.handleNetworkResponse(response)
switch result {
case .failure(FRNetworkingError.invalidAuthToken):
break
// TODO: Get new Access Token and refresh?
default:
break
}
}
Is this the right approach to refresh the token and redo the API call or is there a better way?
You have to write a function that updates the token and, depending on the result, returns true or false
private func refreshAccessToken(completion: #escaping (Bool) -> Void {
// Make a request to refresh the access token
// Update the accessToken and refreshToken variables when the request is completed
// Call completion(true) if the request was successful, completion(false) otherwise
}
Declare 2 variables at the beginning of the class
var session: URLSession
var endpoint: Endpoint
Inside the case .failure assign these variables
session = session
endpoint = endpoint
Then call refreshAccessToken method. The final code will look like this
if let response = response as? HTTPURLResponse {
let result = self.handleNetworkResponse(response)
switch result {
case .failure(FRNetworkingError.invalidAuthToken):
session = session
endpoint = endpoint
self?.refreshAccessToken { success in
if success {
self?.request(using: session, endpoint, completion: completion)
} else {
completion(.failure(.unknownError))
}
}
break
default:
break
}
}

How to retry request with Alamofire?

Is there a way, in Alamofire, to re-send the request if the response code from the first request is 401, where I can refresh the token and retry my request again?
The problem is that I'm using MVVM and also completion handler already.
In my ViewModel the request function looks like:
public func getProfile(completion: #escaping (User?) -> Void) {
guard let token = UserDefaults.standard.value(forKey: Constants.shared.tokenKey) else { return }
let headers = ["Authorization": "Bearer \(token)"]
URLCache.shared.removeAllCachedResponses()
Alamofire.request(Constants.shared.getProfile, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON { (response) in
switch response.result {
case .success:
guard let data = response.data else { return }
if JSON(data)["code"].intValue == 401 {
// here I need to refresh my token and re-send the request
} else {
let user = User(json: JSON(data)["data"])
completion(user)
}
completion(nil)
case .failure(let error):
print("Failure, ", error.localizedDescription)
completion(nil)
}
}
}
and from my ViewController I call it like:
viewModel.getProfile { (user) in
if let user = user {
...
}
}
So I do not know how can retry my request without using a new function, so I can still get my user response from completion part in my ViewController.
Maybe someone can show me the right path.
Thanks in advance!
To retry a request create a Request wrapper and use the RequestInterceptor protocol of Alamofire like this
final class RequestInterceptorWrapper: RequestInterceptor {
// Retry your request by providing the retryLimit. Used to break the flow if we get repeated 401 error
var retryLimit = 0
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
guard let statusCode = request.response?.statusCode else { return }
switch statusCode {
case 200...299:
completion(.doNotRetry)
default:
if request.retryCount < retryLimit {
completion(.retry)
return
}
completion(.doNotRetry)
}
}
//This method is called on every API call, check if a request has to be modified optionally
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
//Add any extra headers here
//urlRequest.addValue(value: "", forHTTPHeaderField: "")
completion(.success(urlRequest))
}
}
Usage: For every API request, the adapt() method is called, and on validate() the retry method is used to validate the status code. retryLimit can be set by creating an instance of the interceptor here
Providing the retryLimit would call the API twice if the response was an error
let interceptor = RequestInterceptorWrapper()
func getDataFromAnyApi(completion: #escaping (User?) -> Void)) {
interceptor.retryLimit = 2
AF.request(router).validate().responseJSON { (response) in
guard let data = response.data else {
completion(nil)
return
}
// convert to User and return
completion(User)
}
}
Yes you can on Alamofire 4.0
The RequestRetrier protocol allows a Request that encountered an Error while being executed to be retried. When using both the RequestAdapter and RequestRetrier protocols together, you can create credential refresh systems for OAuth1, OAuth2, Basic Auth and even exponential backoff retry policies. The possibilities are endless. Here's an example of how you could implement a refresh flow for OAuth2 access tokens.
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
lock.lock() ; defer { lock.unlock() }
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
requestsToRetry.append(completion)
if !isRefreshing {
refreshTokens { [weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
if let accessToken = accessToken, let refreshToken = refreshToken {
strongSelf.accessToken = accessToken
strongSelf.refreshToken = refreshToken
}
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
Reference: AlamofireDocumentation
you can add interceptor
Alamofire.request(Constants.shared.getProfile, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers)
add the protocol RequestInterceptor
then implement this two protocol method
// retryCount number of time api need to retry
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
completion(.success(urlRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
guard request.retryCount < retryCount else {
completion(.doNotRetry)
return
}
/// Call UR API here
}
once api get fail this two method call, do
Could you just recursively call the function if it receives a 401? You would definitely need to create some type of exit condition so that if it continues to fail that it will break out, but it seems to me that it would work.

Centralized token for concurrent asynchronous network tasks

I have an application that is pulling API data for multiple tasks within itself.
The main API data request is to pull the appointments for a given provider or location in our health system.
To have the proper keys to match the provider ID or location ID to the actual name, I pull that data from the API as well to make sure they are up to date.
I have it working so the network call checks if the current API token is active, and if not, automatically refreshes it and re-calls the network data. Once the token is confirmed to either be currently active OR to be refreshed properly, it returns true.
However, the 3 different network pulls are calling data asynchronously and one will update the API token while the other is attempting to do the same and then the API token gets updated repeatedly within the same sequence unnecessarily.
To fix this, I am attempting to switch to a localized token function which checks if the current token is active, and if not refreshes it. It also checks if there is a token update already in progress, and if so stops the other functions from updating it as well. It also re-calls the original function in order to wait for the updated token to appear.
The problem I'm running into is I seem to have created a loop that crashes my app before the new token can appear. Essentially what I'm guessing I need to solve this is to wait for a response before re-calling the loop, but I'm unsure on the best way to do this.
Here is my token check/refresh code:
My model also has a var tokenUpdateInProgress = false so that the new function can alert when a new update begins.
func isTokenActiveAndRefreshIfNot(_ completion: #escaping (Bool) -> ()) {
print("running tokenActive function")
var returnBool = false
print("tokenUpdateInProgressz: \(tokenUpdateInProgress)")
if tokenUpdateInProgress == false {
print("no other update in progress, start the check here")
tokenUpdateInProgress = true
let resourceListURL = "https://api.carecloud.com/v2/oauth2/token_info"
var request = URLRequest(url:URL(string:resourceListURL)!)
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
var json = JSON()
URLSession.shared.dataTask(with: request){ data, response, error in
print("data pull called in isTokenActive")
if let httpResponse = response as? HTTPURLResponse {
print("status code: \(httpResponse.statusCode)")
httpStatusCode = httpResponse.statusCode
}
if httpStatusCode == 200 {
print("token is current, proceed")
tokenUpdateInProgress = false
returnBool = true
json = try! JSON(data: data!)
for (_, _) in json {
if let expiration = json["expires_in"].string {
if let double = Double(expiration) {
let dateTime = Date()
let newDateTime = dateTime.addingTimeInterval(double)
tokenExpiresDateTime = newDateTime
}
}
}
}
if httpStatusCode == 403 || httpStatusCode == 401 {
let downloadGroup = DispatchGroup()
print("token is expired, need to refresh it")
tokenUpdate = false
downloadGroup.enter()
refreshToken({ (tokenRefreshed) in. //this function refreshes my token successfully
if tokenRefreshed == true {
//can do something here if token refresh works correctly
returnBool = true
tokenUpdateInProgress = false
downloadGroup.leave()
downloadGroup.notify(queue: DispatchQueue.main) {
// completion(returnItem)
}
}
})
}
completion(returnBool)
}.resume()
} else if tokenUpdateInProgress == true {
print("token update in progress, re-run check and wait for result")
isTokenActiveAndRefreshIfNot { (result) in //this creates a loop if network not working or token refresh not working for any reason. This loops too fast and leads to a crash.
returnBool = result
}
completion(returnBool)
}
}
The three functions all pulling this data are:
1:
func monthSchedule(resourceID: String, locationID: String, page: Int, _ completion: #escaping ([appt]) -> ()){
isTokenActiveAndRefreshIfNot { (tokenUpdated) in
print("token update within monthschedule: \(tokenUpdated)")
}
... runs network pull/API interpretation from token once confirmed ok
}
2:
func pullLocationsList(_ completion: #escaping ([Int: String]) -> ()){
isTokenActiveAndRefreshIfNot { (tokenReady) in
print("token ready for pull location list: \(tokenReady)")
}
... runs network pull/API interpretation from token once confirmed ok
}
3:
func pullResourceList(_ completion: #escaping ([Int: String]) -> ()){
isTokenActiveAndRefreshIfNot { (tokenReady) in
print("token ready for pull resource list: \(tokenReady)")
}
... runs network pull/API interpretation from token once confirmed ok
}

Alamofire : How to handle errors globally

My question is quite similar to this one, but for Alamofire : AFNetworking: Handle error globally and repeat request
How to be able to catch globally an error (typically a 401) and handle it before other requests are made (and eventually failed if not managed) ?
I was thinking of chaining a custom response handler, but that's silly to do it on each request of the app.
Maybe subclassing, but which class should i subclass to handle that ?
Handling refresh for 401 responses in an oauth flow is quite complicated given the parallel nature of NSURLSessions. I have spent quite some time building an internal solution that has worked extremely well for us. The following is a very high level extraction of the general idea of how it was implemented.
import Foundation
import Alamofire
public class AuthorizationManager: Manager {
public typealias NetworkSuccessHandler = (AnyObject?) -> Void
public typealias NetworkFailureHandler = (NSHTTPURLResponse?, AnyObject?, NSError) -> Void
private typealias CachedTask = (NSHTTPURLResponse?, AnyObject?, NSError?) -> Void
private var cachedTasks = Array<CachedTask>()
private var isRefreshing = false
public func startRequest(
method method: Alamofire.Method,
URLString: URLStringConvertible,
parameters: [String: AnyObject]?,
encoding: ParameterEncoding,
success: NetworkSuccessHandler?,
failure: NetworkFailureHandler?) -> Request?
{
let cachedTask: CachedTask = { [weak self] URLResponse, data, error in
guard let strongSelf = self else { return }
if let error = error {
failure?(URLResponse, data, error)
} else {
strongSelf.startRequest(
method: method,
URLString: URLString,
parameters: parameters,
encoding: encoding,
success: success,
failure: failure
)
}
}
if self.isRefreshing {
self.cachedTasks.append(cachedTask)
return nil
}
// Append your auth tokens here to your parameters
let request = self.request(method, URLString, parameters: parameters, encoding: encoding)
request.response { [weak self] request, response, data, error in
guard let strongSelf = self else { return }
if let response = response where response.statusCode == 401 {
strongSelf.cachedTasks.append(cachedTask)
strongSelf.refreshTokens()
return
}
if let error = error {
failure?(response, data, error)
} else {
success?(data)
}
}
return request
}
func refreshTokens() {
self.isRefreshing = true
// Make the refresh call and run the following in the success closure to restart the cached tasks
let cachedTaskCopy = self.cachedTasks
self.cachedTasks.removeAll()
cachedTaskCopy.map { $0(nil, nil, nil) }
self.isRefreshing = false
}
}
The most important thing here to remember is that you don't want to run a refresh call for every 401 that comes back. A large number of requests can be racing at the same time. Therefore, you want to act on the first 401, and queue all the additional requests until the 401 has succeeded. The solution I outlined above does exactly that. Any data task that is started through the startRequest method will automatically get refreshed if it hits a 401.
Some other important things to note here that are not accounted for in this very simplified example are:
Thread-safety
Guaranteed success or failure closure calls
Storing and fetching the oauth tokens
Parsing the response
Casting the parsed response to the appropriate type (generics)
Hopefully this helps shed some light.
Update
We have now released πŸ”₯πŸ”₯ Alamofire 4.0 πŸ”₯πŸ”₯ which adds the RequestAdapter and RequestRetrier protocols allowing you to easily build your own authentication system regardless of the authorization implementation details! For more information, please refer to our README which has a complete example of how you could implement on OAuth2 system into your app.
Full Disclosure: The example in the README is only meant to be used as an example. Please please please do NOT just go and copy-paste the code into a production application.
in Alamofire 5 you can use RequestInterceptor
Here is my error handling for 401 error in one of my projects, every requests that I pass the EnvironmentInterceptor to it the func of retry will be called if the request get to error
and also the adapt func can help you to add default value to your requests
struct EnvironmentInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
var adaptedRequest = urlRequest
guard let token = KeychainWrapper.standard.string(forKey: KeychainsKeys.token.rawValue) else {
completion(.success(adaptedRequest))
return
}
adaptedRequest.setValue("Bearer \(token)", forHTTPHeaderField: HTTPHeaderField.authentication.rawValue)
completion(.success(adaptedRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
//get token
guard let refreshToken = KeychainWrapper.standard.string(forKey: KeychainsKeys.refreshToken.rawValue) else {
completion(.doNotRetryWithError(error))
return
}
APIDriverAcountClient.refreshToken(refreshToken: refreshToken) { res in
switch res {
case .success(let response):
let saveAccessToken: Bool = KeychainWrapper.standard.set(response.accessToken, forKey: KeychainsKeys.token.rawValue)
let saveRefreshToken: Bool = KeychainWrapper.standard.set(response.refreshToken, forKey: KeychainsKeys.refreshToken.rawValue)
let saveUserId: Bool = KeychainWrapper.standard.set(response.userId, forKey: KeychainsKeys.uId.rawValue)
print("is accesstoken saved ?: \(saveAccessToken)")
print("is refreshToken saved ?: \(saveRefreshToken)")
print("is userID saved ?: \(saveUserId)")
completion(.retry)
break
case .failure(let err):
//TODO logout
break
}
}
} else {
completion(.doNotRetry)
}
}
and you can use it like this :
#discardableResult
private static func performRequest<T: Decodable>(route: ApiDriverTrip, decoder: JSONDecoder = JSONDecoder(), completion: #escaping (AFResult<T>)->Void) -> DataRequest {
return AF.request(route, interceptor: EnvironmentInterceptor())
.responseDecodable (decoder: decoder){ (response: DataResponse<T>) in
completion(response.result)
}

Resources