How to intercept Moya request and return failure response without sending the request at all - ios

I'm using Moya library to handle networking layer, and I already have a custom plugin that add an authentication token to the header.
What I want to do is to make this plugin cancel the request and return a failure response (or throw an error) if the token is not available yet.
P.S. I extended the protocol TargetType to add extra variable that indicates if the target needs authentication or not, so I need to access these data to determine if the authentication token is needed in the header or not.
this is a snapshot of my custom plugin:
struct AuthTokenPlugin: PluginType {
let tokenClosure:()->String?
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
guard let target = target as? AuthorizebleTargetType, target.needsAuth else {
return request
}
guard let token = tokenClosure() else {
// Here where a failure response will be triggered or an error should be thrown
return ......
}
var request = request
request.addValue( "Token " + token, forHTTPHeaderField:"Authorization")
return request
}
}
P.S.2: throwing an error is not a good practice and it is not possible because the enclosing function "prepare(_:target:)" is not declared 'throws'.

I don't think that we can implement such logic with usage of protocol TargetType in cause his methods don't return Bool values and are not throw-marked.
Take a look at MoyaProvider init parameters. There is a requestClosure param in it. You can copy-paste and replace this parameter's default implementation with your own implementation which will check authorization header of Endpoint.
Default implementation of this closure:
final class func defaultRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) {
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
UPD with my comment:
I suggest to check that if Endpoint has header with key “Authorization”, but it’s value is empty string, then call closure parameter with .failure case in requestClosure

Related

Swift - Alamofire - Combine - try to fetch access token if the endpoint returns 401 error code

Ive got an API endpoint that i have got to take some data from but it needs an access token.
This access token is fetched from another endpoint of this api.
The access token expires every 2:30 hrs.
The way I am handling this is that every 2:20 hrs i have a a timer that fetches a new token. I know this is a bad practice since the user might turn off the internet during that fetching etc.
I am using an architectural pattern that splits my main app into 3 seperate layers.
A domain layer which contains all my models, use cases and repositories
A presentation layer which contains all my views and viewmodels.
And a Data layer that contains all my repository implementations , network constants, url builders and my API client where the request is made with alamofire.
My ApiClient is this :
public enum ApiClient {
static func requestCodable<T: Codable>(_ urlConvertible: URLRequestConvertible) -> AnyPublisher<DataResponse<T, NetworkErrorResponse>, Never> {
return AF.request(urlConvertible)
.validate()
.publishDecodable(type: T.self, emptyResponseCodes: [200])
.map { response in
response.mapError { error in
let backendError = response.data.flatMap { try? JSONDecoder().decode(BackendError.self, from: $0) }
return NetworkErrorResponse(initialError: error, backendError: backendError)
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
So , lets assume that i am making a call to an endpoint that needs the access token, and it fails because 3 hours have gone by ...
How can I say to my api client to fetch a new access token and then retry the endpoint that failed ?
Thanks for any help in advance.
Since I'm not aware of your implementation I'll try to give you what I think is a possible solution:
public enum ApiClient {
static func requestCodable<T: Codable>(_ urlConvertible: URLRequestConvertible, isRetry: Bool = false) -> AnyPublisher<DataResponse<T, NetworkErrorResponse>, Never> {
return AF
.request(urlConvertible)
.validate()
.publishDecodable(type: T.self, emptyResponseCodes: [200])
.map { response in
response.mapError { error in
let backendError = response.data.flatMap { try? JSONDecoder().decode(BackendError.self, from: $0) }
return NetworkErrorResponse(initialError: error, backendError: backendError)
}
}
.flatMap { result -> AnyPublisher<DataResponse<T, NetworkErrorResponse>, Never> in
if !isRetry && result == "401 error code" {
return requestAccessToken()
.flatMap({
// probably build a new urlConvertible with the new token
self.requestCodable(urlConvertible, isRetry: true)
})
} else {
return Just(result).eraseToAnyPublisher()
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
You could add a flatMap after your map and verify if you got the 401 error. If so, you would need to get the token, then use flatMap to re-request the requestCodable. You might notice there's a isRetry param, it is there to avoid loops.

RxSwift - Recursive Observables?

Learning RxSwift - Here's my Problem:
i have a webservice that fetches data using an active access token, whenever the token expired , then first call the token generate api and then call the current request to run again. so that it will have an active access token to valid results.
but i have problem in getting the response for token and then call the prev. request?
so i tried adding an observable request , then in response check if the token is invalid, then call another observable to return an active token, once token is received , call the older request again.
func apirequest(_ urlConvertible:URLRequestConvertible) -> Observable<[String:AnyObject]> {
return Observable.create({ observer -> Disposable in
let _ = Alamofire.request(urlConvertible).responseJSON
{ response in
if isTokenExpired() {
self.generateToken().subscribe(onNext: response {
self.apirequest(oldRequest)
})
}
}
return Disposables.create()
})
}
i was expecting like any Rx operators or any ideas to try?
Thanks
I wrote an article about how to do this: https://medium.com/#danielt1263/retrying-a-network-request-despite-having-an-invalid-token-b8b89340d29
Wrap your network calling code in something like this:
/// Builds and makes network requests using the token provided by the service. Will request a new token and retry if the result is an unauthorized (401) error.
///
/// - Parameters:
/// - response: A function that sends requests to the network and emits responses. Can be for example `URLSession.shared.rx.response`
/// - tokenAcquisitionService: The object responsible for tracking the auth token. All requests should use the same object.
/// - request: A function that can build the request when given a token.
/// - Returns: response of a guaranteed authorized network request.
public func getData<T>(response: #escaping (URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)>, 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) }
}
The article and accompanying gist shows how to write the tokenAcquisitionService and includes unit tests.

Moya rxswift : Refresh token and restart request

I'm using Moya Rx swift and i want to catch the response if the status code is 401 or 403 then call refresh token request then recall/retry the original request again and to do so i followed this Link but i tweaked it a bit to suit my needs
public extension ObservableType where E == Response {
/// Tries to refresh auth token on 401 errors and retry the request.
/// If the refresh fails, the signal errors.
public func retryWithAuthIfNeeded(sessionServiceDelegate : SessionProtocol) -> Observable<E> {
return self.retryWhen { (e: Observable<Error>) in
return Observable
.zip(e, Observable.range(start: 1, count: 3),resultSelector: { $1 })
.flatMap { i in
return sessionServiceDelegate
.getTokenObservable()?
.filterSuccessfulStatusAndRedirectCodes()
.mapString()
.catchError {
error in
log.debug("ReAuth error: \(error)")
if case Error.StatusCode(let response) = error {
if response.statusCode == 401 || response.statusCode == 403 {
// Force logout after failed attempt
sessionServiceDelegate.doLogOut()
}
}
return Observable.error(error)
}
.flatMapLatest({ responseString in
sessionServiceDelegate.refreshToken(responseString: responseString)
return Observable.just(responseString)
})
}}
}
}
And my Protocol :
import RxSwift
public protocol SessionProtocol {
func doLogOut()
func refreshToken(responseString : String)
func getTokenObservable() -> Observable<Response>?
}
But it is not working and the code is not compiling, i get the following :
'Observable' is not convertible to 'Observable<_>'
I'm just talking my first steps to RX-swift so it may be simple but i can not figure out what is wrong except that i have to return a type other than the one I'm returning but i do not know how and where to do so.
Your help is much appreciated and if you have a better idea to achieve what I'm trying to do, you are welcome to suggest it.
Thanks in advance for your help.
You can enumerate on error and return the String type from your flatMap. If the request succeeded then it will return string else will return error observable
public func retryWithAuthIfNeeded(sessionServiceDelegate: SessionProtocol) -> Observable<E> {
return self.retryWhen { (error: Observable<Error>) -> Observable<String> in
return error.enumerated().flatMap { (index, error) -> Observable<String> in
guard let moyaError = error as? MoyaError, let response = moyaError.response, index <= 3 else {
throw error
}
if response.statusCode == 401 || response.statusCode == 403 {
// Force logout after failed attempt
sessionServiceDelegate.doLogOut()
return Observable.error(error)
} else {
return sessionServiceDelegate
.getTokenObservable()!
.filterSuccessfulStatusAndRedirectCodes()
.mapString()
.flatMapLatest { (responseString: String) -> Observable<String> in
sessionServiceDelegate.refreshToken(responseString: responseString)
return Observable.just(responseString)
}
}
}
}
Finally i was able to solve this by doing the following :
First create a protocol like so ( Those functions are mandatory and not optional ).
import RxSwift
public protocol SessionProtocol {
func getTokenRefreshService() -> Single<Response>
func didFailedToRefreshToken()
func tokenDidRefresh (response : String)
}
It is very very important to conform to the protocol SessionProtocol in the class that you write your network request(s) in like so :
import RxSwift
class API_Connector : SessionProtocol {
//
private final var apiProvider : APIsProvider<APIs>!
required override init() {
super.init()
apiProvider = APIsProvider<APIs>()
}
// Very very important
func getTokenRefreshService() -> Single<Response> {
return apiProvider.rx.request(.doRefreshToken())
}
// Parse and save your token locally or do any thing with the new token here
func tokenDidRefresh(response: String) {}
// Log the user out or do anything related here
public func didFailedToRefreshToken() {}
func getUsers (page : Int, completion: #escaping completionHandler<Page>) {
let _ = apiProvider.rx
.request(.getUsers(page: String(page)))
.filterSuccessfulStatusAndRedirectCodes()
.refreshAuthenticationTokenIfNeeded(sessionServiceDelegate: self)
.map(Page.self)
.subscribe { event in
switch event {
case .success(let page) :
completion(.success(page))
case .error(let error):
completion(.failure(error.localizedDescription))
}
}
}
}
Then, I created a function that returns a Single<Response>.
import RxSwift
extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Response {
// Tries to refresh auth token on 401 error and retry the request.
// If the refresh fails it returns an error .
public func refreshAuthenticationTokenIfNeeded(sessionServiceDelegate : SessionProtocol) -> Single<Response> {
return
// Retry and process the request if any error occurred
self.retryWhen { responseFromFirstRequest in
responseFromFirstRequest.flatMap { originalRequestResponseError -> PrimitiveSequence<SingleTrait, ElementType> in
if let lucidErrorOfOriginalRequest : LucidMoyaError = originalRequestResponseError as? LucidMoyaError {
let statusCode = lucidErrorOfOriginalRequest.statusCode!
if statusCode == 401 {
// Token expired >> Call refresh token request
return sessionServiceDelegate
.getTokenRefreshService()
.filterSuccessfulStatusCodesAndProcessErrors()
.catchError { tokeRefreshRequestError -> Single<Response> in
// Failed to refresh token
if let lucidErrorOfTokenRefreshRequest : LucidMoyaError = tokeRefreshRequestError as? LucidMoyaError {
//
// Logout or do any thing related
sessionServiceDelegate.didFailedToRefreshToken()
//
return Single.error(lucidErrorOfTokenRefreshRequest)
}
return Single.error(tokeRefreshRequestError)
}
.flatMap { tokenRefreshResponseString -> Single<Response> in
// Refresh token response string
// Save new token locally to use with any request from now on
sessionServiceDelegate.tokenDidRefresh(response: try! tokenRefreshResponseString.mapString())
// Retry the original request one more time
return self.retry(1)
}
}
else {
// Retuen errors other than 401 & 403 of the original request
return Single.error(lucidErrorOfOriginalRequest)
}
}
// Return any other error
return Single.error(originalRequestResponseError)
}
}
}
}
What this function do is that it catches the error from the response then check for the status code, If it is any thing other than 401 then it will return that error to the original request's onError block but if it is 401 (You can change it to fulfill your needs but this is the standard) then it is going to do the refresh token request.
After doing the refresh token request, it checks for the response.
=> If the status code is in bigger than or equal 400 then this means that the refresh token request failed too so return the result of that request to the original request OnError block.
=> If the status code in the 200..300 range then this means that refresh token request succeeded hence it will retry the original request one more time, if the original request fails again then the failure will go to OnError block as normal.
Notes:
=> It is very important to parse & save the new token after the refresh token request is successful and a new token is returned, so when repeating the original request it will do it with the new token & not with the old one.
The token response is returned at this callback right before repeating the original request.
func tokenDidRefresh (response : String)
=> In case the refresh token request fails then it may that the token is expired so in addition that the failure is redirected to the original request's onError, you also get this failure callback
func didFailedToRefreshToken(), you can use it to notify the user that his session is lost or log him out or anything.
=> It is very important to return the function that do the token request because it is the only way the refreshAuthenticationTokenIfNeeded function knows which request to call in order to do the refresh token.
func getTokenRefreshService() -> Single<Response> {
return apiProvider.rx.request(.doRefreshToken())
}
Instead of writing an extension on Observable there's another solution. It's written on pure RxSwift and returns a classic error in case of fail.
The easy way to refresh session token of Auth0 with RxSwift and Moya
The main advantage of the solution is that it can be easily applicable for different services similar to Auth0 allowing to authenticate users in mobile apps.

iOS/Swift: Good architecture approach for connecting REST APIs

I’m developing iOS Apps for quite a long time now. But in the end I was never satisfied with the architecture design for my network layer. Especially when it goes about connecting an API.
There exists a possible duplicate here, but I think my question is more specific as you will see.
Best architectural approaches for building iOS networking applications (REST clients)
I’m not looking for answers like "use AFNetworking/Alamofire". This question is regardless of which 3rd party framework is used.
I mean, often we have the scenario:
"Develop an app X that uses API Y"
And this includes mainly the same steps - everytime.
Implement login / registration
You get an authentication token, have to save it in the keychain and append it in every API call
You have to re-authenticate and re-send the API request which failed with a 401
You have error codes to handle (how to handle them centralized?)
You implement the different API calls.
One problem with 3)
In Obj-C I used NSProxy for intercepting every API Call before it was send, re-authenticated the user if the token expired and and fired the actual request.
In Swift we had some NSOperationQueue where we queued an auth call if we got a 401 and queued the actual request after successful refresh. But that limited us to use a Singleton (which I don’t like much) and we also had to limit the concurrent requests to 1.
I like more the second approach - but is there a better solution?
Regarding 4)
How do you handle http status codes? Do you use many different classes for every error? Do you centralize general error handling in one class? Do you handle them all at the same level or do you catch server errors earlier? (Maybe in your API Wrapper of any 3rd party lib)
How are you developers trying to solve this problems? Have you figured out a "best match" design?
How do you test your APIs? Especially how do you do this in Swift (with no real mocking possibility?).
Of course: Every use case, every app, every scenario is different - there is no "One solution fits them all". But I think these general problems re-appear so often, so I’m tempted to say "Yes, for these cases - there could be one and more solutions - which you can reuse every time".
Looking forward to interesting answers!
Cheers
Orlando 🍻
But that limited us to use a Singleton (which I don’t like much) and we also had to limit the concurrent requests to 1. I like more the second approach - but is there a better solution?
I am using a few layers for authenticating with an API.
Authentication Manager
This manager is responsible for all authentication related functionality. You can think about authentication, reset password, resend verification code functions, and so on.
struct AuthenticationManager
{
static func authenticate(username:String!, password:String!) -> Promise<Void>
{
let request = TokenRequest(username: username, password: password)
return TokenManager.requestToken(request: request)
}
}
In order to request a token we need a new layer called the TokenManager, which manages all things related to a token.
Token Manager
struct TokenManager
{
private static var userDefaults = UserDefaults.standard
private static var tokenKey = CONSTANTS.userDefaults.tokenKey
static var date = Date()
static var token:Token?
{
guard let tokenDict = userDefaults.dictionary(forKey: tokenKey) else { return nil }
let token = Token.instance(dictionary: tokenDict as NSDictionary)
return token
}
static var tokenExist: Bool { return token != nil }
static var tokenIsValid: Bool
{
if let expiringDate = userDefaults.value(forKey: "EXPIRING_DATE") as? Date
{
if date >= expiringDate
{
return false
}else{
return true
}
}
return true
}
static func requestToken(request: TokenRequest) -> Promise<Void>
{
return Promise { fulFill, reject in
TokenService.requestToken(request: request).then { (token: Token) -> Void in
setToken(token: token)
let today = Date()
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)
userDefaults.setValue(tomorrow, forKey: "EXPIRING_DATE")
fulFill()
}.catch { error in
reject(error)
}
}
}
static func refreshToken() -> Promise<Void>
{
return Promise { fulFill, reject in
guard let token = token else { return }
let request = TokenRefresh(refreshToken: token.refreshToken)
TokenService.refreshToken(request: request).then { (token: Token) -> Void in
setToken(token: token)
fulFill()
}.catch { error in
reject(error)
}
}
}
private static func setToken (token:Token!)
{
userDefaults.setValue(token.toDictionary(), forKey: tokenKey)
}
static func deleteToken()
{
userDefaults.removeObject(forKey: tokenKey)
}
}
In order to request a token we'll need a third layer called TokenService which handles all the HTTP calls. I use EVReflection and Promises for my API calls.
Token Service
struct TokenService: NetworkService
{
static func requestToken (request: TokenRequest) -> Promise<Token> { return POST(request: request) }
static func refreshToken (request: TokenRefresh) -> Promise<Token> { return POST(request: request) }
// MARK: - POST
private static func POST<T:EVReflectable>(request: T) -> Promise<Token>
{
let headers = ["Content-Type": "application/x-www-form-urlencoded"]
let parameters = request.toDictionary(.DefaultDeserialize) as! [String : AnyObject]
return POST(URL: URLS.auth.token, parameters: parameters, headers: headers, encoding: URLEncoding.default)
}
}
Authorization Service
I am using an Authorisation Service for the problem you are describing here. This layer is responsible for intercepting server errors such as 401 (or whatever code you want to intercept) and fix them before returning the response to the user. With this approach everything is handled by this layer and you don't have to worry about an invalid token anymore.
In Obj-C I used NSProxy for intercepting every API Call before it was send, re-authenticated the user if the token expired and and fired the actual request. In Swift we had some NSOperationQueue where we queued an auth call if we got a 401 and queued the actual request after successful refresh. But that limited us to use a Singleton (which I don’t like much) and we also had to limit the concurrent requests to 1. I like more the second approach - but is there a better solution?
struct AuthorizationService: NetworkService
{
private static var authorizedHeader:[String: String]
{
guard let accessToken = TokenManager.token?.accessToken else
{
return ["Authorization": ""]
}
return ["Authorization": "Bearer \(accessToken)"]
}
// MARK: - POST
static func POST<T:EVObject> (URL: String, parameters: [String: AnyObject], encoding: ParameterEncoding) -> Promise<T>
{
return firstly
{
return POST(URL: URL, parameters: parameters, headers: authorizedHeader, encoding: encoding)
}.catch { error in
switch ((error as NSError).code)
{
case 401:
_ = TokenManager.refreshToken().then { return POST(URL: URL, parameters: parameters, encoding: encoding) }
default: break
}
}
}
}
Network Service
The last part will be the network-service. In this service layer we will do all interactor-like code. All business logic will end up here, anything related to networking. If you briefly review this service you'll note that there is no UI-logic in here, and that's for a reason.
protocol NetworkService
{
static func POST<T:EVObject>(URL: String, parameters: [String: AnyObject]?, headers: [String: String]?, encoding: ParameterEncoding) -> Promise<T>
}
extension NetworkService
{
// MARK: - POST
static func POST<T:EVObject>(URL: String,
parameters: [String: AnyObject]? = nil,
headers: [String: String]? = nil, encoding: ParameterEncoding) -> Promise<T>
{
return Alamofire.request(URL,
method: .post,
parameters: parameters,
encoding: encoding,
headers: headers).responseObject()
}
}
Small Authentication Demo
An example implementation of this architecture would be a authenticate HTTP request to login a user. I'll show you how this is done using the architecture described above.
AuthenticationManager.authenticate(username: username, password: password).then { (result) -> Void in
// your logic
}.catch { (error) in
// Handle errors
}
Handling errors is always a messy task. Every developer has it's own way of doing this. On the web there are heaps of articles about error handling in for example swift. Showing my error handling will be of not much help since it's just my personal way of doing it, it's also a lot of code to post in this answer, so I rather skip that.
Anyway...
I hope I've helped you back on track with this approach. If there is any question regarding to this architecture, I'll be more than happy to help you out with it. In my opinion there is no perfect architecture and no architecture that can be applied to all projects.
It's a matter of preference, project requirements and expertise in within your team.
Best of luck and please do no hesitate to contact me if there's any problem!

How to handle the response of all types of requests in one handler, but also uniquely handle every request with Alamofire and Moya

In my app I use Moya and Alamofire (And also Moya/RxSwift and Moya-ObjectMapper) libraries for all network requests and responses.
I would like to handle the response of all types of requests in one handler, but also uniquely handle every request.
For example for any request I can get the response "Not valid Version", I would like to avoid to check in every response if this error arrived.
Is there an elegant way to handle this use case with Moya?
Apparently that is very simple, You just should create your own plugin. And add it to your Provider instance (You can add it in the init function)
For example:
struct NetworkErrorsPlugin: PluginType {
/// Called immediately before a request is sent over the network (or stubbed).
func willSendRequest(request: RequestType, target: TargetType) { }
/// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
func didReceiveResponse(result: Result<Moya.Response, Moya.Error>, target: TargetType) {
let responseJSON: AnyObject
if let response = result.value {
do {
responseJSON = try response.mapJSON()
if let response = Mapper<GeneralServerResponse>().map(responseJSON) {
switch response.status {
case .Failure(let cause):
if cause == "Not valid Version" {
print("Version Error")
}
default:
break
}
}
} catch {
print("Falure to prase json response")
}
} else {
print("Network Error = \(result.error)")
}
}
}
I suggest to use generic parametrized method.
class DefaultNetworkPerformer {
private var provider: RxMoyaProvider<GitHubApi> = RxMoyaProvider<GitHubApi>()
func performRequest<T:Mappable>(_ request: GitHubApi) -> Observable<T> {
return provider.request(request).mapObject(T.self)
}
}
DefaultNetworkPerformer will handle all requests from you Moya TargetType. In my case it was GitHubApi. Example usage of this implementation is:
var networkPerformer = DefaultNetworkPerformer()
let observable: Observable<User> = networkPerformer.performRequest(GitHubApi.user(username: "testUser"))
here you 'inform' network performer that response will contain User object.
observable.subscribe {
event in
switch event {
case .next(let user):
//if mapping will succeed here you'll get an Mapped Object. In my case it was User that conforms to Mappable protocol
break
case .error(let error):
//here you'll get MoyaError if something went wrong
break
case .completed:
break
}
}

Resources