Alamofire 3: cancel request with custom error - ios

(Using iOS 9, Swift 2, Alamofire 3)
My app interacts with a REST service that requires a session token in the header. If the session token isn't already available before manager.request() is called, there's no reason to send the request only to have it fail. So I'd like to abort the request with my own error, and have the request's chained response handler take care of it - i.e. the caller wouldn't know that the request was actually never sent to the server.
What's the best way to do this with Alamofire 3?
Any way to have the same effect as Request.cancel() but with custom error, as below?:
static func request(method: Alamofire.Method, _ URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil) -> Request {
let api = apiManager.sharedInstance
guard let sessionToken = api.sessionToken else {
let req = api.alamofireManager.request(method, URLString, parameters)
req.cancel() // ***I'd like to do: req.cancelWithError(.NoSessionToken)
return req
}
// Create request and add session token to header
// ...
}

Related

Refreshing an Access Token p2/OAuth2 iOS

I'm using p2/OAuth2 with Alamofire v4 as explained in documentation here
let sessionManager = SessionManager()
let retrier = OAuth2RetryHandler(oauth2: <# your OAuth2 instance #>)
sessionManager.adapter = retrier
sessionManager.retrier = retrier
self.alamofireManager = sessionManager // you must hold on to this somewhere
// Note that the `validate()` call here is important
sessionManager.request("https://api.github.com/user").validate().responseJSON { response in
debugPrint(response)
}
import Foundation
import OAuth2
import Alamofire
class OAuth2RetryHandler: RequestRetrier, RequestAdapter {
let loader: OAuth2DataLoader
init(oauth2: OAuth2) {
loader = OAuth2DataLoader(oauth2: oauth2)
}
/// Intercept 401 and do an OAuth2 authorization.
public func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion) {
if let response = request.task?.response as? HTTPURLResponse, 401 == response.statusCode, let req = request.request {
var dataRequest = OAuth2DataRequest(request: req, callback: { _ in })
dataRequest.context = completion
loader.enqueue(request: dataRequest)
loader.attemptToAuthorize() { authParams, error in
guard error?.asOAuth2Error != .alreadyAuthorizing else {
// Don't dequeue requests if we are waiting for other authorization request
return
}
self.loader.dequeueAndApply() { req in
if let comp = req.context as? RequestRetryCompletion {
comp(nil != authParams, 0.0)
}
}
}
}
else {
completion(false, 0.0) // not a 401, not our problem
}
}
/// Sign the request with the access token.
public func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
guard nil != loader.oauth2.accessToken else {
return urlRequest
}
return try urlRequest.signed(with: loader.oauth2) // "try" added in 3.0.2
}
}
Everything is working fine, but what I want to achieve is to avoid 401 errors by fetching an access token if expired before submitting a request.
Is it possible to achieve this approach ?
Thanks in advance,
Regards,
Well the standard behaviour in a mobile app should be as follows:
Login and get access token + refresh token
Optionally store tokens in secure storage so that logins on every app restart are avoided
Use access token to call the API and handle 401 responses via a token renewal
Use the refresh token to renew the access token when needed
Eventually the refresh token expires and the user has to login again
You can never completely avoid 401 responses from the API, and UIs need to handle this response specially. It is possible as an optimisation to refresh tokens silently in the background, before the 'exp' claim from the current access token is reached.
Out of interest there is an iOS Code Sample of mine that is easy to run, and which allows a kind of testing of the expiry events. This may give you some ideas on how to adapt your own solution.

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

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

Calling api giving same response even response data is changed

I am new to iOS and want to call an API for login. I am using Alamofire for HTTP request. I am getting a response from the server but I guess it is saving response in cache. So if I call api with different data is printing same response again. And one more question is I want to save session for later api call. How can I save session data and use it in header for later api call?
This is my Login API Call
let params = ["identity": txtId.text!, "pass": txtPassword.text!]
AF.request(LOGIN_URL, method: .post, parameters: params as Parameters).responseJSON { response in
print("response = \(response.result.value)")
}
One easy way to remove caches is to call this code before your network call:
URLCache.shared.removeAllCachedResponses()
Or you can also remove caches for a single request:
let urlRequest = URLRequest(url: URL(string: "https://...")!)
URLCache.shared.removeCachedResponse(for: urlRequest)
Then, make your network call:
let params = ["identity": txtId.text!, "pass": txtPassword.text!]
AF.request(LOGIN_URL, method: .post, parameters: params as Parameters).responseJSON { response in
print("response = \(response.result.value)")
}

Save and resend Alamofire request

I am using Alamofire and I want to send a get request. If this fails, I want to retry it again after some other operations.
I save the request in a variable:
let myRequest = Alamofire.request("myurl", method: .get)
and in another function:
func retry(request:DataRequest) {
request.responseSwiftyJSON { response in
// ... code to handle the response ...
}
}
In this way, I can call multiple times the retry function, passing the same object myRequest.
However, the request is sent correctly only the first time, then I think it is cached (or in some way the Alamofire object realizes that the response field of the request object is already valid) and the request is not sent again to the server.
To solve this problem, I tried to change a little bit the retry function in this way:
func retry2(request:DataRequest) {
Alamofire.request(request.request!).responseSwiftyJSON { response in
// ... code to handle the response ...
}
}
This should initialize a new Alamofire request using only the URLRequest field of the saved request, but now the request is called twice every time! (I check it in my backend and I am sure this is caused by using this approach).
Is there any way to resend the original saved request? Does Alamofire have some way to initialize a new request from a saved one?
Solution
I ended up by doing something like #JonShier and #DuncanC suggested me:
struct APIRequest {
let url: URLConvertible
let method: HTTPMethod
let parameters: Parameters?
let encoding: ParameterEncoding
let headers: HTTPHeaders?
init(_ url:URLConvertible, method:HTTPMethod = .get, parameters:Parameters? = nil, encoding:ParameterEncoding = URLEncoding.default, headers:HTTPHeaders? = nil) {
self.url = url
self.method = method
self.parameters = parameters
self.encoding = encoding
self.headers = headers
}
func make() -> DataRequest {
return Alamofire.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers)
}
}
I haven't used AlamoFire much, and that minimal use was a couple of years ago.
Looking around in the AlamoFire code, it appears that the request() function returns a DataRequest object. Further, it looks like a DataRequest is a reference type that is meant to track a specific request to a server and it's responses.
It looks to make like once you create one, you use it until it completes or fails, and then discard it. They do not appear to be intended to be reused for subsequent requests of the same type.
To summarize:
Question: "How do I reuse AlamoFire DataRequest objects."
Answer: "Don't do that."

How can I use Alamofire to add auth header to each request, and do something if the response is 401?

How can I use Alamofire networking framework to add Authorization header to each request, and also do a check whether the response was 401 from the server, so that I can do logic accordingly, and also present some kind of a login view.
Would I need to create some kind of an HttpService class that wraps Alamofire requests? Or is there a more built in way?
As per to your requirement, I would personally prefer to have a intermediate class for calling methods of Alamofire.
For that you can add auth header on each web service call.
Here is the following example for Intermediate class.
WebClient.swift
class WebClient: SessionManager {
static let sharedManager = WebClient()
func responseRequest(_ url: String, method: Alamofire.HTTPMethod, parameter: Parameters? = nil, encoding: ParameterEncoding, header: HTTPHeaders? = nil, completionHandler: #escaping (DefaultDataResponse) -> Void) -> Void {
self.request(url, method: method, parameters: parameter, encoding: encoding, headers: header).response { response in
completionHandler(response)
}
} }
You can modify above class as per requirement or you can directly pass header in request method for each webservice call.
Adding authorization headers for HTTP Basic Auth and checking the status code is fairly easy:
Alamofire
.request(...)
.authenticate(user: "username", password: "password")
.response { response in
if let status = response.response?.statusCode, status == 401 {
// got a 401 response
}
}

Resources