How to restart a net request with moya ,rxswift - ios

I want to handle each request, and if the response of the request not match the condition , start a new request and get the response. How can I restart the old request
here is my code now
static func request(target: API) -> Observable<Response> {
let actualRequest = provider.request(target)
return self.provider.request(target).flatMapLatest { (response) -> Observable<Response> in
let responseModel = ResponseModel(data:response.data)
if responseModel.code == -405 {
let refreshToken = User.shared?.refreshToken
self.provider.request(.refreshToken(refresh: refreshToken!)).flatMap({ response -> Observable<String> in
return Observable.just("")
}).shareReplay(1).subscribe(onNext: { refreshToken in
// here I get a new token, how to retry the actualRequest , or how to start a new network request with the target
}, onError: { (error) in
},onCompleted: { _ in
})
}
return Observable.just(response)
}
}

You should subclass MoyaProvider, override request method and there you can check response from the api, and retry if needed.

Related

Escaping closure captures non-escaping parameter 'function' Xcode says

I'm trying to perform a segue after two API requests are complete, and I have all the data I need. I'm trying do this because the requests take a bit long. However, when I tried to do something like this post, I got these errors:
1. Passing a non-escaping function parameter 'anotherFunc' to a call to a non-escaping function parameter can allow re-entrant modification of a variable
2. Escaping closure captures non-escaping parameter 'anotherFunc'
3. Escaping closure captures non-escaping parameter 'function'
I added comments where the errors show up. Here's my code:
func getUsernameRequest(function: () -> Void) {
// send an API request to get the userinfo
let url = "\(K.API.baseAPIEndpoint)/user"
let headers: HTTPHeaders = [...]
AF.request(url, headers: headers).responseDecodable(of: UserModel.self) { response in // this is where the #3 error shows up
if let value = response.value {
self.username = value.username
// and do other things
function()
} else {
offlineBanner()
}
}
}
func getMessagesRequest(function: (()->Void)->Void, anotherFunc:()->Void) {
let postDetailUrl = "\(K.API.baseAPIEndpoint)/posts/\(postArray[indexPath.row].id)"
let headers: HTTPHeaders = [...]
// send an API request to get all the associated messages
AF.request(postDetailUrl, headers: headers).responseDecodable(of: PostDetailModel.self) { response in // this is the #2 error shows up
if let value = response.value {
self.messages = value.messages
function(anotherFunc) // this is where the #1 error shows up
} else {
offlineBanner()
}
}
}
func performSegueToChat() -> Void {
self.performSegue(withIdentifier: K.Segue.GotoChat, sender: self)
}
getMessagesRequest(function: getUsernameRequest, anotherFunc: performSegueToChat)
Thank you in advance.
I did this the following code, and it works as I expected! (But they aren't too clean)
func getUsernameRequest(function: #escaping() -> Void) {
// send an API request to get the username
let url = "\(K.API.baseAPIEndpoint)/user"
AF.request(url, headers: headers).responseDecodable(of: GetUsernameModel.self) { response in
if let value = response.value {
self.username = value.username
function()
} else {
offlineBanner()
}
}
}
func getMessagesRequest(function: #escaping(#escaping()->Void)->Void, anotherFunc: #escaping()->Void) {
let postDetailUrl = "\(K.API.baseAPIEndpoint)/posts/\(postArray[indexPath.row].id)"
// send an API request to get all the associated messages and put them into messages array
AF.request(postDetailUrl, headers: headers).responseDecodable(of: PostDetailModel.self) { response in
if let value = response.value {
self.messages = value.messages
function(anotherFunc)
} else {
offlineBanner()
}
}
}
func performSegueToChat() -> Void {
self.performSegue(withIdentifier: K.Segue.GotoChat, sender: self)
}
getMessagesRequest(function: getUsernameRequest, anotherFunc: performSegueToChat)

Alamofire/RxSwift how to refresh token and retry requests automatically on status code 401

I need help with automatically retrying requests after i get first 401 status code on any request. I'm using RxSwift and Alamofire so the call looks like this:
public func getSomeEndpointInfo() -> Observable<PKKInfo> {
return Observable.create({ observer in
let request = AF.request(Router.info)
request
.responseDecodable(of: Info.self) { response in
print("response: \(response)")
if response.response?.statusCode == 401 {
observer.onError(NetworkError.unauthorized)
}
guard let decodedItems = response.value else {
observer.onError(NetworkError.invalidJSON)
return
}
observer.onNext(decodedItems)
observer.onCompleted()
}
return Disposables.create()
})
}
Now in some service I have the following code:
service.getSomeEndpointInfo()
.observe(on: MainScheduler.instance)
.subscribe { [unowned self] info in
self._state.accept(.loaded)
} onError: { [unowned self] error in
print("---> Error")
self.sessionManager
.renewToken()
.observe(on: MainScheduler.instance)
.subscribe { token in
print("---> recieved new token")
self.service.getSomeEndpointInfo()
} onError: { error in
print("---> error generating token")
}.disposed(by: self.disposeBag)
}.disposed(by: disposeBag)
With this code works but I have to call renew token on every request and its embedded into error subscription which doesn't feel well. If you have some other suggestion that on 401 I somehow retry requests and trigger renew token before that i would be grateful.
I wrote an article on how to do this. RxSwift and Handling Invalid Tokens.
The article comes complete with code and tests proving functionality. The key is the class at the bottom of this answer.
You use it like this:
typealias Response = (URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)>
func getData<T>(response: #escaping Response, tokenAcquisitionService: TokenAcquisitionService<T>, request: #escaping (T) throws -> URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable
.deferred { tokenAcquisitionService.token.take(1) }
.map { try request($0) }
.flatMap { response($0) }
.map { response in
guard response.response.statusCode != 401 else { throw TokenAcquisitionError.unauthorized }
return response
}
.retryWhen { $0.renewToken(with: tokenAcquisitionService) }
}
You can use currying to make a function that shares the service...
func makeRequest(builder: #escaping (MyTokenType) -> URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
getData(
response: { URLSession.shared.rx.response(request: $0) /* or however Moya makes network requests */ },
tokenAcquisitionService: TokenAcquisitionService<MyTokenType>(
initialToken: getSavedToken(),
getToken: makeRenewTokenRequest(oldToken:),
extractToken: extractTokenFromData(_:)),
request: builder)
}
Use the above function anywhere in your code that needs token renewal.
Here is the TokenAquisitionService used above. Have all your requests use the same service object.
public final class TokenAcquisitionService<T> {
/// responds with the current token immediatly and emits a new token whenver a new one is aquired. You can, for example, subscribe to it in order to save the token as it's updated.
public var token: Observable<T> { get }
public typealias GetToken = (T) -> Observable<(response: HTTPURLResponse, data: Data)>
/// Creates a `TokenAcquisitionService` object that will store the most recent authorization token acquired and will acquire new ones as needed.
///
/// - Parameters:
/// - initialToken: The token the service should start with. Provide a token from storage or an empty string (object represting a missing token) if one has not been aquired yet.
/// - getToken: A function responsable for aquiring new tokens when needed.
/// - extractToken: A function that can extract a token from the data returned by `getToken`.
public init(initialToken: T, getToken: #escaping GetToken, extractToken: #escaping (Data) throws -> T)
/// Allows the token to be set imperativly if necessary.
/// - Parameter token: The new token the service should use. It will immediatly be emitted to any subscribers to the service.
public func setToken(_ token: T)
}
extension ObservableConvertibleType where Element == Error {
/// Monitors self for `.unauthorized` error events and passes all other errors on. When an `.unauthorized` error is seen, the `service` will get a new token and emit a signal that it's safe to retry the request.
///
/// - Parameter service: A `TokenAcquisitionService` object that is being used to store the auth token for the request.
/// - Returns: A trigger that will emit when it's safe to retry the request.
public func renewToken<T>(with service: TokenAcquisitionService<T>) -> Observable<Void>
}

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.

iOS rxSwift: retryWhen updating refresh token

I have a static function calling a network service.
When the 400 response code happens I would like to redo the network call.
The current code is working, except that the refreshToken in the header does not update between one try and another.
I think that the problem is because the Observable created but the request function does not update at the retry.
I rode on the web that I should use a deferred method on the Observable, but I don't know how.
I've tried moving the code: headers = [HeaderKeys.refreshToken.rawValue: "test test"] anywhere but still it never makes a call with the "test test" refresh token. it always uses the old one.
How can I fix this?
static func getAccessToken() -> Observable<GetAccessTokenResponse> {
var retryCounter = 0
let maxRetryCounter = 3
let delayRetry = 10.0
guard let refreshToken = NetworkHelper.shared.refreshToken else {
return Observable.error(AuthenticationError.networkError)
}
var headers = [HeaderKeys.refreshToken.rawValue: refreshToken]
return NetworkHelper.shared
.request(url: CoreAPI.accessToken.url, request: nil, headers: headers, responseType: GetAccessTokenResponse.self, method: .get, encoding: nil)
.catchError({ (error) -> Observable<(GetAccessTokenResponse?, Int)> in
return Observable.error(AuthenticationError.networkError)
})
.flatMap({ (response) -> Observable<GetAccessTokenResponse> in
// check http status code
switch response.1 {
case 200:
guard response.0?.accessToken != nil else {
return Observable.error(AuthenticationError.genericError)
}
// success
return Observable.just(response.0!)
case 400:
// invalid parameters, refresh token not existing
return Observable.error(AuthenticationError.invalidParameters)
case 404:
// user not existing
return Observable.error(AuthenticationError.userDoesntExist)
default:
// by default return network error
return Observable.error(AuthenticationError.networkError)
}
})
.retryWhen({ (errors) -> Observable<Void> in
return errors
.do(onNext: { (error) in
headers = [HeaderKeys.refreshToken.rawValue: "test test"]
})
.flatMap({error -> Observable<Int> in
debugLog("Retrying get refresh token")
if retryCounter >= maxRetryCounter {
let authError = error as? AuthenticationError ?? .genericError
if authError == AuthenticationError.invalidParameters {
// publish logged false on subject
VDAAuthenticationManager.shared.logged.onNext(false)
}
return Observable.error(error)
}
// increase the retry counter and retry
retryCounter += 1
return Observable<Int>.timer(delayRetry, scheduler: MainScheduler.instance)
})
.flatMap ({ (_) -> Observable<Void> in
return Observable.just(())
})
})
}
In the article RxSwift and Retrying a Network Request Despite Having an Invalid Token I explain how to keep and update a token and how to handle retries when you get a 401 error. Using deferred is part of the answer.
In your particular case. It looks like you could use my service like this:
func getToken(lastResponse: GetAccessTokenResponse?) -> Observable<(response: HTTPURLResponse, data: Data)> {
guard let refreshToken = lastResponse?.refreshToken else { return Observable.error(AuthenticationError.networkError) }
var request = URLRequest(url: CoreAPI.accessToken.url)
request.addValue(refreshToken, forHTTPHeaderField: HeaderKeys.refreshToken.rawValue)
return URLSession.shared.rx.response(request: request)
}
func extractToken(data: Data) throws -> GetAccessTokenResponse {
return try JSONDecoder().decode(GetAccessTokenResponse.self, from: data)
}
let tokenService = TokenAcquisitionService(initialToken: nil, getToken: getToken, extractToken: extractToken(data:))
In the above, you will have to pass a valid initialToken instead of nil or you will have to modify the getToken so it can get a token even if it doesn't have a refresh token.
An example of how to use deferred is below:
let response = Observable
.deferred { tokenAcquisitionService.token.take(1) }
.flatMap { makeRequest(withToken: $0) }
.map { response in
guard response.response.statusCode != 401 else { throw ResponseError.unauthorized }
return response
}
.retryWhen { $0.renewToken(with: tokenAcquisitionService) }
I explain in the article what each line of code is for and how it works.

Resources