Disclaimer: I'm new to iOS programming, so this question is probably as simple as it looks. It's not a trick question!
I've a Swift project that uses Almofire to send HTTP requests. I want to add a parameter to the query string for every single request made.
So, I want to add mykey=myval to every request.
EG: http://example.com/index -> http://example.com/index?mykey=myval
EG: http://example.com/index?key=val -> http://example.com/index?key=val&mykey=myval
I have found that all requests seem to go through
public func request(URLRequest: URLRequestConvertible) -> Request {
return Manager.sharedInstance.request(URLRequest.URLRequest)
}
in a file named Almofire.swift
and also through
public func request(URLRequest: URLRequestConvertible) -> Request {
var dataTask: NSURLSessionDataTask?
dispatch_sync(queue) {
dataTask = self.session.dataTaskWithRequest(URLRequest.URLRequest)
}
let request = Request(session: session, task: dataTask!)
delegate[request.delegate.task] = request.delegate
if startRequestsImmediately {
request.resume()
}
return request
}
in a file named Manager.swift, so I'm presuming I need to add a bit of code here. Due to my lack of Swift knowledge I've spend hours experimenting but no joy - only exceptions.
Does anyone know how I can add a parameter to all requests?
You don't need to change anything in Alamofire's code. Instead you can use the URLRequestConvertible protocol to encapsulate your URLs and parameter in an enum:
enum Router: URLRequestConvertible {
static let baseURLString = "https://example.com" // define your base URL here
static var defaultParams = ["myKey": "myValue"] // set the default params here
// define a case for every request you need
case Index
case Endpoint1(param: String)
case Endpoint2(param1: String, param2: String)
var URLRequest: NSMutableURLRequest {
let result: (path: String, parameters: [String: AnyObject]) = {
// set the path and params for each request
switch self {
case .Index:
return ("/index", Router.defaultParams)
case .Endpoint1(let param):
var params = Router.defaultParams
params.updateValue(param, forKey: "key")
return ("/endpoint", params)
case .Endpoint2(let param1, let param2):
var params = Router.defaultParams
params.updateValue(param1, forKey: "key1")
params.updateValue(param2, forKey: "key2")
return ("/endpoint2", params)
}
}()
// create the URL and the request
let URL = NSURL(string: Router.baseURLString)!
let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
let encoding = Alamofire.ParameterEncoding.URL
return encoding.encode(URLRequest, parameters: result.parameters).0
}
}
Then you can call your requests in the following matter:
// sends a request to 'https://example.com/index?myKey=myValue'
Alamofire.request(Router.Index).response { (request, urlResponse, data, error) -> Void in
// handle response
}
// sends a request to 'https://example.com/endpoint?key=value&myKey=myValue'
Alamofire.request(Router.Endpoint1(param: "value")).response { (request, urlResponse, data, error) -> Void in
// handle response
}
// sends a request to 'https://example.com/endpoint2?key1=value1&key2=value2&myKey=myValue'
Alamofire.request(Router.Endpoint2(param1: "value1", param2: "value2")).response { (request, urlResponse, data, error) -> Void in
// handle response
}
Simple request ->
func someFunction()
{
Alamofire.request(.GET, "apiName", parameters:["Key":"Value"])
.response { request, response, data, error in
if error == nil {
print(request)
print(response)
}
else {
//Display Error Message
print(error)
}
}
}
This is another solution of adding default parameters to every urlRequest. You have to create your class that conforms to RequestInterceptor protocol, and define "adapt" method:
class UserRequestInterceptor: RequestInterceptor {
static var defaultParameters = ["mykey": "myval"]
public func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
let encoding = URLEncodedFormParameterEncoder.default
if let request = try? encoding.encode(UserRequestInterceptor.defaultParameters, into: urlRequest) {
urlRequest = request
}
completion(.success(urlRequest))
}
}
and when you decide to create an URL request you have to add "interceptor" parameter:
func userFunction()
{
AF.request("http://example.com/index", method: .get,
interceptor: UserRequestInterceptor()) // this line will be add all default params to every request
.response { response in
// ...
})
}
or you can create new Alamofire Session and init it with UserRequestInterceptor:
class UserClass {
let afSession = Session(interceptor: GlobusPro.SDK.GlobusProRequestInterceptor())
...
func userFunction()
{
afSession.request("http://example.com/index", method: .get)
.response { response in
// ...
})
}
}
Related
I have been working on an interceptor for adding auth headers to network requests in my app.
final class AuthInterceptor: URLProtocol {
private var token: String = "my.access.token"
private var dataTask: URLSessionTask?
private struct UnexpectedValuesRepresentationError: Error { }
override class func canInit(with request: URLRequest) -> Bool {
guard URLProtocol.property(forKey: "is_handled", in: request) as? Bool == nil else { return false }
return true
//return false // URL Loading System will handle the request using the system’s default behavior
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
guard let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else { return }
URLProtocol.setProperty(true, forKey: "is_handled", in: mutableRequest)
mutableRequest.addValue(token, forHTTPHeaderField: "Authorization")
dataTask = URLSession.shared.dataTask(with: mutableRequest as URLRequest) { [weak self] data, response, error in
guard let self = self else { return }
if let error = error {
self.client?.urlProtocol(self, didFailWithError: error)
} else if let data = data, let response = response {
self.client?.urlProtocol(self, didLoad: data)
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
} else {
self.client?.urlProtocol(self, didFailWithError: UnexpectedValuesRepresentationError())
}
self.client?.urlProtocolDidFinishLoading(self)
}
dataTask?.resume()
}
override func stopLoading() {
dataTask?.cancel()
dataTask = nil
}
}
As you can see I am currently just using private var token: String = "my.access.token" to mock a token. I'd like to introduce a TokenLoader that will fetch my token from it's cache.
As the URL Loading system will initialize instances of my protocol as needed, I am not sure how I can inject this. It is currently registered using: URLProtocol.registerClass(AuthInterceptor.self)
Imagine I had an interface like this -
public typealias LoadTokenResult = Result<String, Error>
public protocol TokenLoader {
func load(_ key: String, completion: #escaping (LoadTokenResult) -> Void)
}
I'd like to ensure this is testable so I'd expect to be able to stub this in a test case or use a spy.
How can I achieve this?
As it is seen AuthInterceptor does not depend on TokenProvider by instance level, so you can inject class level dependency and use shared TokenProvider to extract/load tokens (for instance, depending on URL). This gives possibility to inject cached token provider for testing easily
final class AuthInterceptor: URLProtocol {
static var tokenProvider: TokenLoader? // << inject here
// initial value, will be loaded from to tokenProvider result
private var token: String?
// ... other code
// somewhere inside when needed, like
Self.tokenProvider?.load(key) { [weak self] result in
self?.token = try? result.get()
// proceed with token if present ...
}
}
so in UT scenario you can do something like
override func setUp {
AuthInterceptor.tokenProvider = MockTokenProvider()
}
override func tearDown {
AuthInterceptor.tokenProvider = nil // or some default if needed
}
One possible option could be to use Generics for this.
protocol TokenProvider {
static var token: String {get}
}
class MockTokenProvider: TokenProvider {
static var token: String = "my.access.token"
}
And change AuthInterceptor like so:
final class AuthInterceptor<T: TokenProvider>: URLProtocol {
private var token: String = T.token
....
...
}
And register like so:
URLProtocol.registerClass(AuthInterceptor<MockTokenProvider>.self)
the above compiles in a playground, haven't tried it in a real project though..
I have a question about the AuthenticationInterceptor added in Alamofire 5.2.
I am using AuthenticationInterceptor to refresh the oAuth token.
https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#authenticationinterceptor
The code I'm trying is below.
struct OAuthCredential: AuthenticationCredential {
let accessToken: String
let refreshToken: String
let userID: String
let expiration: Date
// Require refresh if within 5 minutes of expiration
var requiresRefresh: Bool { Date(timeIntervalSinceNow: 60 * 5) > expiration }
}
class OAuthAuthenticator: Authenticator {
func apply(_ credential: OAuthCredential, to urlRequest: inout URLRequest) {
urlRequest.headers.add(.authorization(bearerToken: credential.accessToken))
}
func refresh(_ credential: OAuthCredential,
for session: Session,
completion: #escaping (Result<OAuthCredential, Error>) -> Void) {
// Request to refresh token
let request = Session.default.request(
URL(string: "URL for token refresh")!,
method: .patch,
parameters: ["refresh_token": credential.refreshToken]
)
request.responseJSON { response in
// Get new Credential information
// ~~~~~~
completion(.success(newCredential))
}
}
func didRequest(_ urlRequest: URLRequest,
with response: HTTPURLResponse,
failDueToAuthenticationError error: Error) -> Bool {
return response.statusCode == 401
}
func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: OAuthCredential) -> Bool {
let bearerToken = HTTPHeader.authorization(bearerToken: credential.accessToken).value
return urlRequest.headers["Authorization"] == bearerToken
}
}
// ~~~~~~
let session = Session.default
let urlRequest = try! URLRequest(
url: URL(string: "https://api.example/docs")!,
method: .get
)
session.request(urlRequest, interceptor: interceptor).responseJSON { response in
// ~~~~~
}
What I don't know is whether didRequest and isRequest are sometimes called. I debugged while pasting a breakpoint when I got a 401 error, but it didn't stop at the above method.
The reason those methods weren't called was that they would always be returned on the line below.
https://github.com/Alamofire/Alamofire/blob/4f72b95b49c22e445e1866712f719698fa11c30c/Source/AuthenticationInterceptor.swift#L297
I couldn't find a case that didn't go through this line.
I would like to know the case where two methods are called.
try to call a .validate()
session.request(urlRequest, interceptor: interceptor).validate().responseJSON { response in
// ~~~~~
}
I have an API class with the following function:
static func JSONPostRequest<EncodableType:Codable, DecodableType:Codable>(endpoint: String, jsonData: EncodableType, callback: #escaping (DecodableType) -> Void, clientErrorCallback: #escaping (Error) -> Void, responseErrorCallback: #escaping (URLResponse) -> Void, requestHeaders: [String: String]?) {
// Encoding the data into JSON
var jsonData: Data = Data();
do {
jsonData = try JSONEncoder().encode(jsonData)
}
catch {
print("JSON Encode")
return ;
}
// Setup the request
var request: URLRequest = URLRequest(url: URL(string: self.endpoint + self.apiVersion + "/" + endpoint)!)
request.httpMethod = "POST"
request.httpBody = jsonData
// Set the custom headers
if(requestHeaders != nil) {
for (headerKey, headerValue) in requestHeaders! {
request.setValue(headerValue, forHTTPHeaderField: headerKey)
}
}
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Was there an error in request?
if error != nil {
clientErrorCallback(error!)
return
}
// Response code is 2XX?
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
responseErrorCallback(response!)
return
}
// Has mime type fine?
guard let mime = response!.mimeType, mime == "application/json" else {
print("Wrong MIME type!")
return
}
let decoder = JSONDecoder()
let loginResponse = try! decoder.decode(DecodableType.self, from: data!)
callback(loginResponse)
}
task.resume()
}
But when I would like to call it from my login view ... :
API.JSONPostRequest(
endpoint: "login",
jsonData: LoginRequest(username: self.username, password: self.password),
callback: { loginResponse in
// success callback
if loginResponse.success && loginResponse.token != "" {
callback(loginResponse)
}
else {
// backend logic error
}
}, clientErrorCallback: { error in
// client error
}, responseErrorCallback: { urlResponse in
// response error
}, requestHeaders: headers
)
... I get the following error message:
Generic parameter 'EncodableType' could not be inferred
Here's my LoginRequest implementation:
struct LoginRequest: Codable {
var username: String;
var password: String;
}
The error message shows near the first line of the function call, near "API.JSONPostRequest(".
What could be the problem here?
You didn't provide a type for loginResponse, and there's no way from this code to guess what it is. Looking at the code you've posted, I can't figure out what the response is supposed to be (nothing you've posted here has a .success or .token value).
I assume you meant to add the following code:
struct LoginResponse: Codable {
var success: Bool
var token: String
}
And I assume somewhere there is a variable callback of type (LoginResponse) -> Void.
With that type, you'll still need to let the compiler know it's what you expect. It has no way to guess of the infinite number of types this could return, that you want LoginResponse:
...
callback: { (loginResponse: LoginResponse) in
...
Alternately, you can adjust your type signature to pass the expected type, for example:
static func JSONPostRequest<Request, Response>(endpoint: String,
jsonData: Request,
returning: Response.Type = Response.self,
callback: #escaping (Response) -> Void,
clientErrorCallback: #escaping (Error) -> Void,
responseErrorCallback: #escaping (URLResponse) -> Void,
requestHeaders: [String: String]?)
where Request: Encodable, Response: Decodable {
This would allow you to pass returning: LoginResponse.self, which can be nicer than embedding it in the closure:
...
returning: LoginResponse.self,
callback: { loginResponse in
...
(That said, what I'm discussing here fixes the fact that DecodableType is actually ambiguous. The fact that you're getting an error on EncodableType suggests that you have other type-mismatches. I recommend simplifying this to a minimal example that actually demonstrates the problem in a playground. As you've written it, I had to guess at a bunch of extra code.)
I'm in the process of converting to Swift 3.0, and have a custom Router class for Alamofire. My code is as follows:
enum Router: URLRequestConvertible {
case get(query: String, params: [String: AnyObject]?)
case post(query: String, params: [String: AnyObject]?)
case put(query: String, params: [String: AnyObject]?)
case delete(query: String, params: [String: AnyObject]?)
var urlRequest: NSMutableURLRequest {
// Default to GET
var httpMethod: HTTPMethod = .get
let (path, parameters): (String, [String: AnyObject]?) = {
switch self {
case .get(let query, let params):
// Set the request call
httpMethod = .get
// Return the query
return (query, params)
case .post(let query, let params):
// Set the request call
httpMethod = .post
// Return the query
return (query, params)
case .put(let query, let params):
// Set the request call
httpMethod = .put
// Return the query
return (query, params)
case .delete(let query, let params):
// Set the request call
httpMethod = .delete
// Return the query
return (query, params)
}
}()
// Create the URL Request
let urlRequest: NSMutableURLRequest
if let url = URL(string: Globals.BASE_URL + path) {
urlRequest = NSMutableURLRequest(url: url)
} else {
urlRequest = NSMutableURLRequest()
}
// set header fields
if let key = UserDefaults.standard.string(forKey: Globals.NS_KEY_SESSION) {
urlRequest.setValue(key, forHTTPHeaderField: "X-XX-API")
}
// Add user agent
if let userAgent = UserDefaults.standard.string(forKey: Globals.NS_KEY_USER_AGENT) {
urlRequest.setValue(userAgent, forHTTPHeaderField: "User-Agent")
}
// Set the HTTP method
urlRequest.httpMethod = httpMethod.rawValue
// Set timeout interval to 20 seconds
urlRequest.timeoutInterval = 20
return Alamofire.URLEncoding().encode(urlRequest as! URLRequestConvertible, with: parameters)
}
func asURLRequest() throws -> URLRequest {
}
}
This is giving me an error stating Type of expression is ambiguious without more context at Alamofire.URLEncoding(). What does this mean?
Within the Alamofire docs at https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%204.0%20Migration%20Guide.md#parameter-encoding-protocol, they have the code
public protocol ParameterEncoding {
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}
This is nearly a duplicate of How to migrate Alamofire router class to Swift 3?
Check it out and you'll see how to use the new ParameterEncoding and especially that URLRequestConvertible is now implemented with a func asURLRequest() throws -> URLRequest and not a var anymore.
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)
}