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!
Related
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.
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.
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
I am using Moya to handle HTTP operations and normally I have an refreshToken(). I am checking token if expired or not when a request is about happen but the problem is there can be a scenarios that more than one requests. If they are chained with nested types it is not a problem however, it is not likely all the time.
To be more clear lets say I have request1() and request2() and assume that they execute separate operations and can be triggered anytime(for instance one is called in a viewDidLoad(), other one is called in another viewDidLoad()). when this happens and if the token is expired, my refresh request fails. (statusCode: 400) So, my question is, how can I make provider to wait refresh() operation get done?I mean by provider is other endpoints. I want them to wait refresh() endpoint if it is on.
I will be very appreciated if you suggest a way that will make this easier.
I just set an variable called isTokenRefreshing true when i start the refresh() operation and checked it before making a request. If it was true I stored all the requests in an array and when the refresh() is finished I executed another function which basically makes all the stored requests in a for loop.
If anyone wants to see the code I can share. Just let me know.
EDIT
This where I, NetworkManager, handle all my requests. It is in an Singleton class.
private var awaitingRequests : [NetworkAPI] = []
func makeRequest(_ request: NetworkAPI){
if (Token.sharedInstance.isTokenRefreshing && request.requiresToken) {
self.awaitingRequests.append(request)
return
}
self.provider.request(){ result in ... }
}
func executeWaitedRequests(){
for request in self.awaitingRequests {
self.makeRequest(request)
}
}
NetworkAPI is main enum that I hold my endpoint cases. See the Moya documents if you do not what I am talking about.
And this is where I handle my Token operations.
class Token {
static let sharedInstance = Token()
private init(){}
var isTokenRefreshing: Bool = false
func refresh(_ completion: #escaping ()->()){
self.isTokenRefreshing = true
print("refreshing token")
let queue = DispatchQueue(label: "com.asd.ads.makeRequest", attributes: DispatchQueue.Attributes.concurrent)
queue.sync(flags: .barrier, execute: {
NetworkManager.shared.makeRequest(.refresh(), completionHandler: { (success, error) in
self.isTokenRefreshing = false
if success{
completion()
NetworkManager.shared.executeWaitedRequests()
}
print("refrehing ended!")
})
})
}
}
I'm struggling with getting this to work to make request to my API. Without a token works, but when I try to add additional headers, things turn to be complicated, for me.
First, the structure.
one class called: APIAsyncTask that makes the requests
one class called APIParams, just a data holder to send parameters to the APIAsyncTask class.
one class called DatabaseAPI that makes that builds the parameters, and send that to the APIAsyncTask class.
DatabaseAPI
func someMethod()
{
let task = APIAsyncTasks()
task.registerCallback { (error, result) -> Void in
print("Finished task, back at DatabaseAPI")
}
let params2 = APIParams(request: .GET, apiPath: "Posts/1", apiToken: "4iTX-56w")
task.APIrequest(params2)
}
APIAsyncTask
This part is for fixing another error, because manager was not global, the task got cancelled quickly.
var manager : Manager!
init(authenticatedRequest : Bool, token: String?)
{
manager = Alamofire.Manager()
print("Pre \(manager.session.configuration.HTTPAdditionalHeaders?.count)")
if(authenticatedRequest && token != nil)
{
var defaultHeaders = Alamofire.Manager.sharedInstance.session.configuration.HTTPAdditionalHeaders!
defaultHeaders["Authorization"] = "bearer \(token)"
let configuration = Manager.sharedInstance.session.configuration
configuration.HTTPAdditionalHeaders = defaultHeaders
manager = Alamofire.Manager(configuration: configuration)
}
print("Post \(manager.session.configuration.HTTPAdditionalHeaders?.count)")
}
After some decision making, it comes down to this part.
private func GetRequest(url: String!,token : String?, completionHandler: (JSON?, NSURLRequest?, NSHTTPURLResponse?, NSError?) -> () ) -> ()
{
print("Begin Get Request")
if(token != nil)//if token is not nil, make authenticated request
{
print("just before request: \(manager.session.configuration.HTTPAdditionalHeaders?.count)")
manager.request(.GET, url, parameters: nil, encoding: .JSON).responseJSON { (request, response, json, error) in
print("Get Request (authenticated), inside alamofire request")
var resultJson : JSON?
if(json != nil)
{
resultJson = JSON(json!)
}
completionHandler(resultJson, request, response, error)
}
}
else
{
//working part without token
So as the code is now, I get an error on completing:
Mattt himself gives the answer of using Alamofire.Manager.sharedInstance.session.configuration.HTTPAdditionalHeaders
, so that should be fine...
I suspect it has something to do with the multiple threads, according to this blog. Or, since it is something about CFNetwork, it could be because my API does not use SSL? I disabled NSAppTransportSecurity
I'm kind of new to swift, so examples would be really appreciated! Thankyou!
So the majority of your code looks solid.
The error leads me to believe that CFNetwork is having difficulty figuring out how to compute the protection space for the challenge. I would also assume you are getting a basic auth challenge since you are attaching an Authorization header.
Digging through your logic a bit more with this in mind led me to see that your not attaching your token to the string properly inside the Authorization header. You need to do the following instead.
defaultHeaders["Authorization"] = "bearer \(token!)"
Otherwise your Authorization header value is going to include Optional(value) instead of just value.
That's the only issue I can see at the moment. If you could give that a try and comment back that would be great. I'll update my answer accordingly if that doesn't actually solve your problem.
Best of luck!
You can add your headers in your request with Alamofire 2 and Swift 2.
For an example: go to example