Alamofire RequestRetrier with timeout error handling - ios

I'm using RequestRetrier to automatically renew access_token for my API. But in each request function I would like to catch timeout error, but .case(let error) in .responseJSON body never executes, because of (I guess) retrier that I add to my accessSessionManager. Here's how it looks like:
lazy var accessSessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = Configuration.timeout
configuration.timeoutIntervalForResource = Configuration.timeout
let sessionManager = Alamofire.SessionManager(configuration: configuration)
let oAuth2Handler = OAuth2Handler()
sessionManager.retrier = oAuth2Handler
sessionManager.adapter = oAuth2Handler
return sessionManager
}()
func changeName(newName: String,completionHandler: ((_ success: Bool, _ error: String?) -> ())?) {
guard let accessToken = self.getAccessToken() else { return }
let parameters = ["access_token": accessToken, "name": newName] as [String : Any]
self.accessSessionManager.request(Constants.nameUrl, method: .get, parameters: parameters).responseJSON { response in
switch response.result {
case .success(let json):
let jsonDict = JSON(json)
if let success = jsonDict["success"].bool {
completionHandler?(success, nil)
}
case .failure(let error):
if error._code == NSURLErrorTimedOut {
completionHandler?(false, "Please check your Internet connection and try again!")
} else if response.response?.statusCode == 400 {
completionHandler?(false, "Sorry, name not found")
} else if response.response?.statusCode != 401 {
completionHandler?(false, error.localizedDescription)
}
}
}
}
}
....
....
class OAuth2Handler {
//MARK: - Adapter
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
if let url = urlRequest.url {
guard let accessToken = self.getAccessToken() else { return urlRequest }
let newUrl = addOrUpdateQueryStringParameter(url: "\(url)", key: "access_token", value: accessToken)
let newRequest = URLRequest(url: URL(string: newUrl)!)
return newRequest
}
return urlRequest
}
// MARK: - RequestRetrier
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 {
if 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.updateAccessToken(accessToken: accessToken, refreshToken: refreshToken)
}
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}
}
So, basically error handling performs in should function, not in .case(let error) in my function.

Ok, so there's my very stupid mistake, basically completion(false,0,0) was never executed if error occurred in should function. Everything works if it looks something like this:
// MARK: - RequestRetrier
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 {
if 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.updateAccessToken(accessToken: accessToken, refreshToken: refreshToken)
}
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
} else {
completion(false,0.0)
}
}

You are not validating your request. Therefor it will let every request be a succes. Try validating your request by adding a validate() after the request but before the response:
self.accessSessionManager.request(...).validate().responseJSON { ... }
You can find custom ways to change the behavior what is and what is not accepted by the validate() function in their readme

Related

How to use dependency injection networking with MVVM iOS?

I'm currently trying to find out what's the best networking architecture for MVVM applications. I couldn't find many resources and decided to go with dependency injection based architecture as per the very less reading resources that I have found.
I'm not using any 3rd party for web service testing and whatever the networking architecture that I use should be supported to mock the web services as well.
I have found out the DI based networking architecture which was build intended to achieve unit testing according to the Test Pyramid concept at Apple WWDC 2018.
So I have build my networking layer according to that. Following is my APIHandler class.
public enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
protocol RequestHandler {
associatedtype RequestDataType
func makeRequest(from data:RequestDataType) -> URLRequest?
}
protocol ResponseHandler {
associatedtype ResponseDataType
func parseResponse(data: Data, response: HTTPURLResponse) throws -> ResponseDataType
}
typealias APIHandler = RequestHandler & ResponseHandler
Followings are my extensions for request handler and response handler.
extension RequestHandler {
func setQueryParams(parameters:[String: Any], url: URL) -> URL {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.queryItems = parameters.map { element in URLQueryItem(name: element.key, value: String(describing: element.value) ) }
return components?.url ?? url
}
func setDefaultHeaders(request: inout URLRequest) {
request.setValue(APIHeaders.contentTypeValue, forHTTPHeaderField: APIHeaders.kContentType)
}
}
struct ServiceError: Error,Decodable {
let httpStatus: Int
let message: String
}
extension ResponseHandler {
func defaultParseResponse<T: Decodable>(data: Data, response: HTTPURLResponse) throws -> T {
let jsonDecoder = JSONDecoder()
if response.statusCode == 200 {
do {
let body = try jsonDecoder.decode(T.self, from: data)
return body
} catch {
throw ServiceError(httpStatus: response.statusCode, message: error.localizedDescription)
}
} else {
var message = "Generel.Message.Error".localized()
do {
let body = try jsonDecoder.decode(APIError.self, from: data)
if let err = body.fault?.faultstring {
message = err
}
} catch {
throw ServiceError(httpStatus: response.statusCode, message: error.localizedDescription)
}
throw ServiceError(httpStatus: response.statusCode, message:message)
}
}
}
Then I loaded my request using APILoader as follows.
struct APILoader<T: APIHandler> {
var apiHandler: T
var urlSession: URLSession
init(apiHandler: T, urlSession: URLSession = .shared) {
self.apiHandler = apiHandler
self.urlSession = urlSession
}
func loadAPIRequest(requestData: T.RequestDataType, completionHandler: #escaping (Int, T.ResponseDataType?, ServiceError?) -> ()) {
if let urlRequest = apiHandler.makeRequest(from: requestData) {
urlSession.dataTask(with: urlRequest) { (data, response, error) in
if let httpResponse = response as? HTTPURLResponse {
guard error == nil else {
completionHandler(httpResponse.statusCode, nil, ServiceError(httpStatus: httpResponse.statusCode, message: error?.localizedDescription ?? "General.Error.Unknown".localized()))
return
}
guard let responseData = data else {
completionHandler(httpResponse.statusCode,nil, ServiceError(httpStatus: httpResponse.statusCode, message: error?.localizedDescription ?? "General.Error.Unknown".localized()))
return
}
do {
let parsedResponse = try self.apiHandler.parseResponse(data: responseData, response: httpResponse)
completionHandler(httpResponse.statusCode, parsedResponse, nil)
} catch {
completionHandler(httpResponse.statusCode, nil, ServiceError(httpStatus: httpResponse.statusCode, message: CommonUtil.shared.decodeError(err: error)))
}
} else {
guard error == nil else {
completionHandler(-1, nil, ServiceError(httpStatus: -1, message: error?.localizedDescription ?? "General.Error.Unknown".localized()))
return
}
completionHandler(-1, nil, ServiceError(httpStatus: -1, message: "General.Error.Unknown".localized()))
}
}.resume()
}
}
}
To call my API request. I have created a separate service class and call the web service as follows.
struct TopStoriesAPI: APIHandler {
func makeRequest(from param: [String: Any]) -> URLRequest? {
let urlString = APIPath().topStories
if var url = URL(string: urlString) {
if param.count > 0 {
url = setQueryParams(parameters: param, url: url)
}
var urlRequest = URLRequest(url: url)
setDefaultHeaders(request: &urlRequest)
urlRequest.httpMethod = HTTPMethod.get.rawValue
return urlRequest
}
return nil
}
func parseResponse(data: Data, response: HTTPURLResponse) throws -> StoriesResponse {
return try defaultParseResponse(data: data,response: response)
}
}
For syncing both my actual web service methods and mock services, I have created an API Client protocol like follows.
protocol APIClientProtocol {
func fetchTopStories(completion: #escaping (StoriesResponse?, ServiceError?) -> ())
}
Then I have derived APIServices class using my APIClient protocol and implemented my all the APIs there by passing requests and responses. My dependency injection was getting over at this point.
public class APIServices: APIClientProtocol {
func fetchTopStories(completion: #escaping (StoriesResponse?, ServiceError?) -> ()) {
let request = TopStoriesAPI()
let params = [Params.kApiKey.rawValue : CommonUtil.shared.NytApiKey()]
let apiLoader = APILoader(apiHandler: request)
apiLoader.loadAPIRequest(requestData: params) { (status, model, error) in
if let _ = error {
completion(nil, error)
} else {
completion(model, nil)
}
}
}
}
Then I have called this API request on my viewModel class like this.
func fetchTopStories(completion: #escaping (Bool) -> ()) {
APIServices().fetchTopStories { response, error in
if let _ = error {
self.errorMsg = error?.message ?? "Generel.Message.Error".localized()
completion(false)
} else {
if let data = response?.results {
if data.count > 0 {
self.stories.removeAll()
self.stories = data
completion(true)
} else {
self.errorMsg = "Generel.NoData.Error".localized()
completion(false)
}
} else {
self.errorMsg = "Generel.NoData.Error".localized()
completion(false)
}
}
}
}
Finally call the viewModel's API call from my viewController (View).
func fetchData() {
showActivityIndicator()
self.viewModel.fetchTopStories { success in
self.hideActivityIndicator()
DispatchQueue.main.async {
if self.pullToRefresh {
self.pullToRefresh = false
self.refreshControl.endRefreshing()
}
if success {
if self.imgNoData != nil {
self.imgNoData?.isHidden = true
}
self.tableView.reloadData()
} else {
CommonUtil.shared.showToast(message: self.viewModel.errorMsg, success: false)
self.imgNoData = {
let viewWidth = self.tableView.frame.size.width
let imageWidth = viewWidth - 50
let iv = UIImageView()
iv.frame = CGRect(x: 25, y: 100, width: imageWidth, height: imageWidth)
iv.image = UIImage(named:"no-data")
iv.contentMode = .scaleAspectFit
return iv
}()
self.imgNoData?.isHidden = false
self.tableView.addSubview(self.imgNoData!)
}
}
}
}
So I have following questions regarding this approach.
I have ended the dependency injection from my APIServices class.
Should I bring this all the way up to my viewController class API Call and
pass request and params variables from there ?
Are there any performance issues in this approach and any
improvement to be done?
My personal preference is to end all the data related stuffs from the viewModel level and just call the API without passing any parameters from the viewController. Does it wrong? If we pass parameters from the view controller class as per the pure dependency injection way, does it harm to the MVVM architecture?

Getting statusCode other than 200...299 in HTTPURLResponse of URLSession

the following is my APIManager code, I'm using it in all my apps. But sometimes, the guard statement fails in connectToServer function, which means the statusCode of HTTPURLResponse other than 200...299 and the thing here is even after getting statusCode other than 200...299 my record got inserted into DB. I don't know what happens.
I thought that the cause of this behavior is from ServerURL, because I'm using a dev server with IP address http://00.000.0.000/ without security. Once I moved it to domain as https://XXX.XXXXXXXXXX.XXXXX/ it is working fine. Can you help me to figure out this?
And also will it supports for asynchronous calls?
import UIKit
struct APIResponse : Decodable {
let status : Bool
let message : String
let extra: String?
}
internal let BASE_URL = "http://00.000.0.00/app/v0_1/api/" // Example server URL
enum APIPath: String {
case registration = "registration"
case login = "login"
case getProfile = "get_profile"
func directURL() -> URL? {
let urlPath = BASE_URL + self.rawValue
return URL(string: urlPath)
}
func extendedURL(using parameters: [String: Any]) -> URL? {
let extendedPath = parameters.map { $0.key + "=" + "\($0.value)" }.joined(separator: "&")
let urlPath = BASE_URL + self.rawValue + "?" + extendedPath
return URL(string: urlPath)
}
}
enum APIMethod: String {
case get = "GET"
case put = "PUT"
case post = "POST"
case patch = "PATCH"
case delete = "DELETE"
}
enum APIHeaders {
case user
case app
var authorization: [String:String] {
let acceptLanguage = UserDefaults.standard.value(forKey: UDKeys.appleLanguage) as? String ?? ""
if self == .user {
let token = UserDefaults.standard.value(forKey: UDKeys.userToken) as? String ?? ""
return ["Content-Type": "application/json", "Accept": "application/json", "Accept-Language": acceptLanguage, "Token" : token]
}
return ["Content-Type": "application/json", "Accept": "application/json", "Accept-Language": acceptLanguage]
}
}
struct APIRequest {
var url: URL?
var method: String
var parameters: Data?
var headers: [String:String]
init(path: APIPath, method: APIMethod, headers: APIHeaders) {
self.url = path.directURL()
self.method = method.rawValue
self.headers = headers.authorization
}
init(path: APIPath, parameters: [String: Any], method: APIMethod, headers: APIHeaders) {
self.url = path.extendedURL(using: parameters)
self.method = method.rawValue
self.headers = headers.authorization
}
init(path: APIPath, method: APIMethod, body: [String:Any], headers: APIHeaders) {
self.url = path.directURL()
self.method = method.rawValue
self.parameters = try? JSONSerialization.data(withJSONObject: body, options: .sortedKeys)
self.headers = headers.authorization
}
init<Encode: Encodable>(path: APIPath, method: APIMethod, body: Encode, headers: APIHeaders) {
self.url = path.directURL()
self.method = method.rawValue
self.parameters = try? JSONEncoder().encode(body)
self.headers = headers.authorization
}
}
struct APIError: Error {
let reason: String
let code: String?
init(reason: String, code: String? = nil) {
self.reason = reason
self.code = code
}
}
struct APIDispatcher {
static let instance = APIDispatcher()
private init() {}
func dispatch<Decode: Decodable>(request: APIRequest, response: Decode.Type, result: #escaping (Result<Decode, APIError>) -> ()) {
DispatchQueue(label: "queue", attributes: .concurrent).async {
self.connectToServer(with: request) { (resultant) in
switch resultant {
case .success(let data):
do {
let decoded = try JSONDecoder().decode(response, from: data)
DispatchQueue.main.async {
result(.success(decoded))
}
} catch let decodedError {
print("[Decoded Error]: ", decodedError)
do {
let apiResponse = try JSONDecoder().decode(APIResponse.self, from: data)
let apiError = APIError(reason: apiResponse.message, code: apiResponse.extra)
DispatchQueue.main.async {
result(.failure(apiError))
}
} catch {
let apiError = APIError(reason: decodedError.localizedDescription)
DispatchQueue.main.async {
result(.failure(apiError))
}
}
}
case .failure(let error):
DispatchQueue.main.async {
result(.failure(error))
}
}
}
}
}
func dispatch(request: APIRequest, result: #escaping (Result<Dictionary<String,Any>, APIError>) -> ()) {
DispatchQueue(label: "queue", attributes: .concurrent).async {
self.connectToServer(with: request) { (resultant) in
switch resultant {
case .success(let data):
do {
let serialized = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! Dictionary<String,Any>
DispatchQueue.main.async {
result(.success(serialized))
}
} catch {
let error = APIError(reason: error.localizedDescription)
DispatchQueue.main.async {
result(.failure(error))
}
}
case .failure(let error):
DispatchQueue.main.async {
result(.failure(error))
}
}
}
}
}
private func connectToServer(with request: APIRequest, result: #escaping (Result<Data, APIError>) -> ()) {
guard let url = request.url else {
let error = APIError(reason: "Invalid URL")
result(.failure(error))
return
}
var urlRequest = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30)
urlRequest.httpMethod = request.method
urlRequest.httpBody = request.parameters
urlRequest.allHTTPHeaderFields = request.headers
print(urlRequest)
let urlSessionConfiguration = URLSessionConfiguration.default
urlSessionConfiguration.waitsForConnectivity = false
urlSessionConfiguration.timeoutIntervalForRequest = 30
urlSessionConfiguration.timeoutIntervalForResource = 60
let urlSession = URLSession(configuration: urlSessionConfiguration)
urlSession.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
let error = APIError(reason: error.localizedDescription)
result(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
let error = APIError(reason: "Server Error")
result(.failure(error))
return
}
if let data = data {
result(.success(data))
}
}.resume()
}
}
Note: BASE_URL and APIResponse might be vary according to project.
I'm using it as
func login() {
self.startLoading()
let body = ["mobile_number": phoneNumberTF.text!, "password" : passwordTF.text!, "uuid" : UIDevice.current.identifierForVendor!.uuidString]
let apiRequest = APIRequest(path: .login, method: .post, body: body, headers: .app)
APIDispatcher.instance.dispatch(request: apiRequest) { result in
self.stopLoading()
switch result {
case .success(let response):
break
case .failure(let error):
break
}
}
}
EDIT: My bad I asked completely reverse on statsCode now I modified it.

Alamofire 5 Adapting and Retrying Requests

I am trying to implement my OAuth2 flow using Alamofire 5.0.0-beta.3. As i can see the documentation is still for Alamofire 4 as stated in the github page as well.
I am trying to make the Oauth2 handler following the documentation for Alamofire 4. As the class names are changed, I am completely lost while making it.
This is the code that i am following:
class OAuth2Handler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void
private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
private let lock = NSLock()
private var clientID: String
private var baseURLString: String
private var accessToken: String
private var refreshToken: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
// MARK: - Initialization
public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
self.clientID = clientID
self.baseURLString = baseURLString
self.accessToken = accessToken
self.refreshToken = refreshToken
}
// MARK: - RequestAdapter
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
return urlRequest
}
return urlRequest
}
// MARK: - RequestRetrier
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)
}
}
// MARK: - Private - Refresh Tokens
private func refreshTokens(completion: #escaping RefreshCompletion) {
guard !isRefreshing else { return }
isRefreshing = true
let urlString = "\(baseURLString)/oauth2/token"
let parameters: [String: Any] = [
"access_token": accessToken,
"refresh_token": refreshToken,
"client_id": clientID,
"grant_type": "refresh_token"
]
sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.responseJSON { [weak self] response in
guard let strongSelf = self else { return }
if
let json = response.result.value as? [String: Any],
let accessToken = json["access_token"] as? String,
let refreshToken = json["refresh_token"] as? String
{
completion(true, accessToken, refreshToken)
} else {
completion(false, nil, nil)
}
strongSelf.isRefreshing = false
}
}
}
This is how to use this for alamofire 4:
let baseURLString = "https://some.domain-behind-oauth2.com"
let oauthHandler = OAuth2Handler(
clientID: "12345678",
baseURLString: baseURLString,
accessToken: "abcd1234",
refreshToken: "ef56789a"
)
let sessionManager = SessionManager()
sessionManager.adapter = oauthHandler
sessionManager.retrier = oauthHandler
let urlString = "\(baseURLString)/some/endpoint"
sessionManager.request(urlString).validate().responseJSON { response in
debugPrint(response)
}
This is the link i am following to implement this.
https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests
Look at something like this.
struct EnvironmentInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest>) -> Void) {
var adaptedRequest = urlRequest
guard let token = AtraqService.shared.user?.token.accessToken else {
completion(.success(adaptedRequest))
return
}
adaptedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
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
}
}
}
Then
Session(configuration: configuration, interceptor: EnvironmentInterceptor())
Finally
request().validate().response...
If Alamofire 5 is not intercepting (adapting or retrying) your requests for some reason, then just try to check the delegate signature out as described here.
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
var modifiedURLRequest = urlRequest
let apiToken = config.apiToken
modifiedURLRequest.setValue(apiToken, forHTTPHeaderField: Constants.apiTokenHeader)
completion(.success(modifiedURLRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
completion(.doNotRetry)
}
Here's the difference (look at the signature):
func adapt(_ urlRequest: URLRequest, for session: Alamofire.Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
var modifiedURLRequest = urlRequest
let apiToken = config.apiToken
modifiedURLRequest.setValue(apiToken, forHTTPHeaderField: Constants.apiTokenHeader)
completion(.success(modifiedURLRequest))
}
func retry(_ request: Alamofire.Request, for session: Alamofire.Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
completion(.doNotRetry)
}

iOS - Alamofire RequestRetrier not provoked

I am trying to implement a retry mechanism and i saw that alamofire has one.
I am trying to implement a simple mechanism of retry with number of times for a request , yet something is wrong.
class OAuth2Handler: RequestAdapter, RequestRetrier {
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
return urlRequest
}
var defaultRetryCount = 4
private var requestsAndRetryCounts: [(Request, Int)] = []
private var lock = NSLock()
private func index(request: Request) -> Int? {
return requestsAndRetryCounts.index(where: { $0.0 === request })
}
func addRetryInfo(request: Request, retryCount: Int? = nil) {
lock.lock() ; defer { lock.unlock() }
guard index(request: request) == nil else { print("ERROR addRetryInfo called for already tracked request"); return }
requestsAndRetryCounts.append((request, retryCount ?? defaultRetryCount))
}
func deleteRetryInfo(request: Request) {
lock.lock() ; defer { lock.unlock() }
guard let index = index(request: request) else { print("ERROR deleteRetryInfo called for not tracked request"); return }
requestsAndRetryCounts.remove(at: index)
}
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion){
lock.lock() ; defer { lock.unlock() }
guard let index = index(request: request) else { completion(false, 0); return }
let (request, retryCount) = requestsAndRetryCounts[index]
if retryCount == 0 {
completion(false, 0)
} else {
requestsAndRetryCounts[index] = (request, retryCount - 1)
completion(true, 0.5)
}
}
}
this is the class that i am trying to use this:
let sessionManager = SessionManager()
override init() {
sessionManager.adapter = RequestAdapter.self as? RequestAdapter
sessionManager.retrier = OAuth2Handler()
}
func sendRequest(url: String,meth: HTTPMethod,parameters: [String: AnyObject]?, success: #escaping (String, Data) -> Void, failure: #escaping (Error) -> Void) {
self.asyncSerialWorker.enqueueWork { (done) in
self.sessionManager.request(url, method:meth).responseJSON { (responseObject) -> Void in
if responseObject.result.isSuccess {
print("Generic succsess")
let value = responseObject.result.value
let json = JSON(value!)
guard let result = responseObject.data else {return}
success(self.parser.parseMaiden(json: json), result)
}
if responseObject.result.isFailure {
let error : Error = responseObject.result.error!
print("login failed")
failure(error)
}
done()
}
}
}
if there are any other suggestions i would love to hear them
thanks
sessionManager.adapter = RequestAdapter.self as? RequestAdapter seems very wrong. You should be setting it to an instance of your OAuth2Handler.
So the issue her was to add the request to the retry, so first i did this:
let sessionManager = SessionManager()
var retrier = OAuth2Handler()
override init() {
sessionManager.retrier = retrier
}
and in the call itself i did as follow:
func sendRequest(url: String,meth: HTTPMethod,parameters: [String: AnyObject]?, success: #escaping (String, Data) -> Void, failure: #escaping (Error) -> Void) {
let request = sessionManager.request(url, method: meth, parameters: parameters, encoding: JSONEncoding.default)
retrier.addRetryInfo(request: request)
self.asyncSerialWorker.enqueueWork { (done) in
self.sessionManager.request(url, method:meth).responseJSON { (responseObject) -> Void in
if responseObject.result.isSuccess {
print("Generic succsess")
let value = responseObject.result.value
let json = JSON(value!)
guard let result = responseObject.data else {return}
success(self.parser.parseMaiden(json: json), result)
}
if responseObject.result.isFailure {
let error : Error = responseObject.result.error!
print("login failed")
failure(error)
}
done()
}
}
}
as you can see i have add to the retry a request :
retrier.addRetryInfo(request: request)
maybe i should do a remove in success(will check and update)

Understanding AlamoFire OAuth Example

I was able to get a working implementation of OAuth example as provided by AlamoFire. However, I am looking to understand certain lines of code and how it works.
Full Example:
class OAuth2Handler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void
private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
private let lock = NSLock()
private var clientID: String
private var baseURLString: String
private var accessToken: String
private var refreshToken: String
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
// MARK: - Initialization
public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
self.clientID = clientID
self.baseURLString = baseURLString
self.accessToken = accessToken
self.refreshToken = refreshToken
}
// MARK: - RequestAdapter
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
return urlRequest
}
return urlRequest
}
// MARK: - RequestRetrier
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)
}
}
// MARK: - Private - Refresh Tokens
private func refreshTokens(completion: #escaping RefreshCompletion) {
guard !isRefreshing else { return }
isRefreshing = true
let urlString = "\(baseURLString)/oauth2/token"
let parameters: [String: Any] = [
"access_token": accessToken,
"refresh_token": refreshToken,
"client_id": clientID,
"grant_type": "refresh_token"
]
sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.responseJSON { [weak self] response in
guard let strongSelf = self else { return }
if
let json = response.result.value as? [String: Any],
let accessToken = json["access_token"] as? String,
let refreshToken = json["refresh_token"] as? String
{
completion(true, accessToken, refreshToken)
} else {
completion(false, nil, nil)
}
strongSelf.isRefreshing = false
}
}
}
Questions:
[weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
What is the purpose of [weak self] and the guard for strongSelf?
requestsToRetry.append(completion)
if !isRefreshing {
refreshTokens { [weak self] succeeded, accessToken, refreshToken in
guard let strongSelf = self else { return }
//Implementation
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
How does this request retry work? The requestsToRetry is just an array of RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) How does it know what requests to retry?
strongSelf.lock.lock()
Does NSLock just not allow self (OAuth2Handler) to be accessed by any other thread while this method is executing?
1) Exactly as commented by Fonix, you have a strong reference to selfto avoid that if self was nil you start to collect retain cycles..
I'm refeer to :
[weak self] ... in
guard let strongSelf = self else { return }
Since self will be captured in the block which is dispatched asynchronously, self will be implicitly retained and released again when the block has been finished, in other words self will be extended up until after the block finishes. Making this code, you avoid to extend the life-time of self and decide to don't execute the block if self is equal to nil
2) According to the lines you mentioned:
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
requestsToRetry.append(completion)
..
there is an array called requestsToRetry which contains all the request you need to relaunch. In this code you append to the array all the request that have the 401 status code (when server returns status code 401)
With the code forEach you loop through the requestToRetry array:
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
and launch all items. After the cycle is concluded you remove all the items.
In fact, the sources report:
public typealias RequestRetryCompletion = (_ shouldRetry: Bool, _ timeDelay: TimeInterval) -> Void
public protocol RequestRetrier {
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: #escaping RequestRetryCompletion)
}
You can find more details here
3) Exactly as you said the frequently concurrency issues faced are the one related to accessing/modifying the shared resource from different threads.
The lock.lock() is a solution to lock the other execution blocks when items is being modified. The code in defer is called just before leaving the function to unlocking the block.

Resources