I think this is a pretty common use case, but I couldn't find any best way to achieve it.
Some parts of my ios application require login. How do I achieve the following pattern using Alamofire and swift.
.request (.GET 'login_required_endpoint')
.responsejson(if statusCode == 401){
login()
continue with GETing
login_required_endpoint
What is the best way to achieve this.
Make a request
If server responds with 401(Unauthorized)
Ask user to login after saving all the request payload for previous request
After successful login, continue with request in [1] with payload saved
(I realize it is open-ended, but any help on how to make progress would be highly appreciated)
With the below, you can call handleRequest instead of Alamofire's request
import Alamofire
import PromiseKit
enum ServerError: ErrorType {
case Unauthorized
// add others as necessary
}
func handleRequest(method: Alamofire.Method, _ URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil) -> Promise<AnyObject> {
return request(method, URLString, parameters: parameters).recover { error -> AnyObject in
switch error {
case ServerError.Unauthorized:
return login().then {
request(method, URLString, parameters: parameters)
}
default:
throw error
}
}
}
private func request(method: Alamofire.Method, _ URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil) -> Promise<AnyObject> {
return Promise { fulfill, reject in
Alamofire.request(method, URLString, parameters: parameters).responseJSON { response in
switch response.result {
case .Success(let json):
fulfill(json)
case .Failure(let error):
if response.response?.statusCode == 401 {
reject(ServerError.Unauthorized)
}
else {
reject(error)
}
}
}
}
}
private func login() -> Promise<Void> {
// do what you need here...
return Promise()
}
Related
I currently have a network client that looks like the below:
class Client<R: ResourceType> {
let engine: ClientEngineType
var session: URLSession
init(engine: ClientEngineType = ClientEngine()) {
self.engine = engine
self.session = URLSession.shared
}
func request<T: Codable>(_ resource: R) -> Single<T> {
let request = URLRequest(resource: resource)
return Single<T>.create { [weak self] single in
guard let self = self else { return Disposables.create() }
let response = self.session.rx.response(request: request)
return response.subscribe(
onNext: { response, data in
if let error = self.error(from: response) {
single(.error(error))
return
}
do {
let decoder = JSONDecoder()
let value = try decoder.decode(T.self, from: data)
single(.success(value))
} catch let error {
single(.error(error))
}
},
onError: { error in
single(.error(error))
})
}
}
struct StatusCodeError: LocalizedError {
let code: Int
var errorDescription: String? {
return "An error occurred communicating with the server. Please try again."
}
}
private func error(from response: URLResponse?) -> Error? {
guard let response = response as? HTTPURLResponse else { return nil }
let statusCode = response.statusCode
if 200..<300 ~= statusCode {
return nil
} else {
return StatusCodeError(code: statusCode)
}
}
}
Which I can then invoke something like
let client = Client<MyRoutes>()
client.request(.companyProps(params: ["collections": "settings"]))
.map { props -> CompanyModel in return props }
.subscribe(onSuccess: { props in
// do something with props
}) { error in
print(error.localizedDescription)
}.disposed(by: disposeBag)
I'd like to start handling 401 responses and refreshing my token and retrying the request.
I'm struggling to find a nice way to do this.
I found this excellent gist that outlines a way to achieve this, however I am struggling to implement this in my current client.
Any tips or pointers would be very much appreciated.
That's my gist! (Thanks for calling it excellent.) Did you see the article that went with it? https://medium.com/#danielt1263/retrying-a-network-request-despite-having-an-invalid-token-b8b89340d29
There are two key elements in handling 401 retries. First is that you need a way to insert tokens into your requests and start your request pipeline with Observable.deferred { tokenAcquisitionService.token.take(1) }. In your case, that means you need a URLRequest.init that will accept a Resource and a token, not just a resource.
The second is to throw a TokenAcquisitionError.unauthorized error when you get a 401 and end your request pipeline with .retryWhen { $0.renewToken(with: tokenAcquisitionService) }
So, given what you have above, in order to handle token retries all you need to do is bring my TokenAcquisitionService into your project and use this:
func getToken(_ oldToken: Token) -> Observable<(response: HTTPURLResponse, data: Data)> {
fatalError("this function needs to be able to request a new token from the server. It has access to the old token if it needs that to request the new one.")
}
func extractToken(_ data: Data) -> Token {
fatalError("This function needs to be able to extract the new token using the data returned from the previous function.")
}
let tokenAcquisitionService = TokenAcquisitionService<Token>(initialToken: Token(), getToken: getToken, extractToken: extractToken)
final class Client<R> where R: ResourceType {
let session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
func request<T>(_ resource: R) -> Single<T> where T: Decodable {
return Observable.deferred { tokenAcquisitionService.token.take(1) }
.map { token in URLRequest(resource: resource, token: token) }
.flatMapLatest { [session] request in session.rx.response(request: request) }
.do(onNext: { response, _ in
if response.statusCode == 401 {
throw TokenAcquisitionError.unauthorized
}
})
.map { (_, data) -> T in
return try JSONDecoder().decode(T.self, from: data)
}
.retryWhen { $0.renewToken(with: tokenAcquisitionService) }
.asSingle()
}
}
Note, it could be the case that the getToken function has to, for example, present a view controller that asks for the user's credentials. That means you need to present your login view controller (or a UIAlertController) to gather the data. Or maybe you get both an authorization token and a refresh token from your server when you login. In that case the TokenAcquisitionService should hold on to both of them (i.e., its T should be a (token: String, refresh: String). Either is fine.
The only problem with the service is that if acquiring the new token fails, the entire service shuts down. I haven't fixed that yet.
I have an app where I used RxSwift for my networking by extending ObservableType this works well but the issue I am having now is when I make an API request and there is an error, I am unable to show the particular error message sent from the server. Now how can I get the particular error response sent from the server
extension ObservableType {
func convert<T: EVObject>(to observableType: T.Type) -> Observable<T> where E: DataRequest {
return self.flatMap({(request) -> Observable<T> in
let disposable = Disposables.create {
request.cancel()
}
return Observable<T>.create({observer -> Disposable in
request.validate().responseObject { (response: DataResponse<T>) in
switch response.result {
case .success(let value):
if !disposable.isDisposed {
observer.onNext(value)
observer.onCompleted()
}
case .failure(let error):
if !disposable.isDisposed {
observer.onError(NetworkingError(httpResponse: response.response,
networkData: response.data, baseError: error))
observer.onCompleted()
}
}
}
return disposable
})
})
}
}
let networkRetryPredicate: RetryPredicate = { error in
if let err = error as? NetworkingError, let response = err.httpResponse {
let code = response.statusCode
if code >= 400 && code < 600 {
return false
}
}
return true
}
// Use this struct to pass the response and data along with
// the error as alamofire does not do this automatically
public struct NetworkingError: Error {
let httpResponse: HTTPURLResponse?
let networkData: Data?
let baseError: Error
}
response from the server could be
{
"status" : "error",
"message" : " INSUFFICIENT_FUNDS"
}
or
{
"status" : "success",
"data" : " gghfgdgchf"
}
my response is handled like this
class MaxResponse<T: NSObject>: MaxResponseBase, EVGenericsKVC {
var data: T?
public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
switch key {
case "data":
data = value as? T
default:
print("---> setGenericValue '\(value)' forUndefinedKey '\(key)' should be handled.")
}
}
public func getGenericType() -> NSObject {
return T()
}
}
the error is
return ApiClient.session.rx.request(urlRequest: MaxApiRouter.topupWall(userId: getUser()!.id!, data: body))
.convert(to: MaxResponse<Wall>.self)
In the official Alamofire docs it is mentioned that validate(), without any parameters:
Automatically validates status code within 200..<300 range, and that
the Content-Type header of the response matches the Accept header of
the request, if one is provided.
So if you do not include Alamofire's validate() you are saying that no matter the status code, if the request did get through, you will consider it successful, so that's why it shows nothing in the failure block.
However if you prefer to use it, yes, it will give you an ResponseValidationFailureReason error, but you still have access to the response.data. Try printing it, you should see the expected error response from the server:
if let responseData = response.data {
print(String(data: responseData, encoding: .utf8))
}
I have a network layer working with generics and I'm using protocols so I can test it later. I have followed this tutorial https://medium.com/thecocoapps/network-layer-in-swift-4-0-972bf2ea5033
This is my Mock for testing:
import Foundation
#testable import TraktTest
class MockUrlSessionProvider: ProviderProtocol {
enum Mode {
case success
case empty
case fail
}
private var mode: Mode
init(mode: Mode) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
switch mode {
case .success: completion(NetworkResponse.success(T))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}
I'm getting the error: Cannot convert value of type 'NetworkResponse<T.Type>' to expected argument type 'NetworkResponse<_>' in this line: completion(NetworkResponse.success(T))
If I send this to my completion success it compile:
try? JSONDecoder().decode(T.self, from: data!)
(dummy data that I created using encode and my model), but crash when get to my model because is nil despite I had encoded using JSONEncoder() with a correct model.
I think it works, because is the same logic that I use in my class that implements ProviderProtocol in my app:
final class URLSessionProvider: ProviderProtocol {
private var session: URLSessionProtocol
init(session: URLSessionProtocol = URLSession.shared) {
self.session = session
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
let request = URLRequest(service: service)
session.dataTask(request: request) { [weak self] data, response, error in
let httpResponse = response as? HTTPURLResponse
self?.handleDataResponse(data: data, response: httpResponse, error: error, completion: completion)
}.resume()
}
private func handleDataResponse<T: Decodable>(data: Data?, response: HTTPURLResponse?, error: Error?, completion: (NetworkResponse<T>) -> Void) {
guard error == nil else { return completion(.failure(.unknown(error?.localizedDescription ?? "Error"))) }
guard let response = response else { return completion(.failure(.unknown("no_response".localized()))) }
switch response.statusCode {
case 200...299:
guard let data = data, let model = try? JSONDecoder().decode(T.self, from: data) else { return completion(.failure(.noData)) }
completion(.success(model))
default: completion(.failure(.unknown("no_response".localized())))
}
}
}
URLSessionProtocol is just a protocol which has a method dataTask same as the one in URLSession.shared (receive a URLRequest and returns Data, Response and Error in a completion).
My Network responses are a couple of enums:
enum NetworkResponse<T> {
case success(T)
case failure(NetworkError)
}
enum NetworkError {
case unknown(String)
case noData
}
My provider protocol just have a function to make the request using generics:
protocol ProviderProtocol {
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping(NetworkResponse<T>) -> Void) where T: Decodable
}
I don't think I need to use ServiceProtocol in my test because is to setup the request with endpoint, headers, body, id, etc. But this is the protocol I created:
typealias Headers = [String: String]
typealias Parameters = [String: Any]
protocol ServiceProtocol {
func baseURL() -> URL
var path: String? { get }
var id: String? { get }
var method: HTTPMethod { get }
var task: Task { get }
var headers: Headers? { get }
var parametersEncoding: ParametersEncoding { get }
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
}
enum Task {
case requestPlain
case requestParameters(Parameters)
}
enum ParametersEncoding {
case url
case json
}
In my app, I have a class that implement ProviderProtocol and use a URLSession.shared to make the dataTask when some viewModel call the request with the appropiate model.
I'm use to make test with protocols and a specific model, but with generics is showing me that error. How can I achieve to have a mock provider using generics so I can test any viewModel who make a call to network using different kinds of models (stubs).
The error occurs because NetworkResponse expects an instance of T, while the mock tries to provide the actual T.
So, you need to somehow provide an instance, however this cannot be generated by the mock as it doesn't have enough information about how to construct an instance.
I recommend injecting the success value from the outside, when creating the mock. You can do this either by making the mock class generic, or by making the Mode enum generic. Below is a sample implementation for the latter:
class MockUrlSessionProvider: ProviderProtocol {
// making the enum generic, to support injecting the success value
enum Mode<T> {
case success(T)
case empty
case fail
}
// need to have this as `Any` to cover all possible T generic arguments
private var mode: Any
// however the initializer can be very specific
init<T>(mode: Mode<T>) {
self.mode = mode
}
func request<T>(type: T.Type, service: ServiceProtocol, completion: #escaping (NetworkResponse<T>) -> Void) where T: Decodable {
// if the mock was not properly configured, do nothing
guard let mode = mode as? Mode<T> else { return }
// alternatively you force cast and have the unit test crash, this should help catching early configuration issues
// let mode = mode as! Mode<T>
switch mode {
case let .success(value): completion(NetworkResponse.success(value))
case .empty: completion(.failure(.noData))
case .fail: completion(.failure(.unknown("Error")))
}
}
}
For my networking module, I have this protocol that I adopt for accessing different parts of the API:
protocol Router: URLRequestConvertible {
var baseUrl: URL { get }
var route: Route { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var encoding: ParameterEncoding? { get }
var responseResultType: Decodable.Type? { get }
}
I'm adopting this with enums that look like this:
enum TestRouter: Router {
case getTestData(byId: Int)
case updateTestData(byId: Int)
var route: Route {
switch self {
case .getTestData(let id): return Route(path: "/testData/\(id)")
case .updateTestData(let id): return Route(path: "/testDataOtherPath/\(id)")
}
}
var method: HTTPMethod {
switch self {
case .getTestData: return .get
case .updateTestData: return .put
}
}
var headers: [String : String]? {
return [:]
}
var encoding: ParameterEncoding? {
return URLEncoding.default
}
var responseResultType: Decodable.Type? {
switch self {
case .getTestData: return TestData.self
case .updateTestData: return ValidationResponse.self
}
}
}
I want to use Codable for decoding nested Api responses. Every response consists of a token and a result which content is depending on the request route.
For making the request I want to use the type specified in the responseResultType property in the enum above.
struct ApiResponse<Result: Decodable>: Decodable {
let token: String
let result: Result
}
extension Router {
func asURLRequest() throws -> URLRequest {
// Construct URL
var completeUrl = baseUrl.appendingPathComponent(route.path, isDirectory: false)
completeUrl = URL(string: completeUrl.absoluteString.removingPercentEncoding ?? "")!
// Create URL Request...
var urlRequest = URLRequest(url: completeUrl)
// ... with Method
urlRequest.httpMethod = method.rawValue
// Add headers
headers?.forEach { urlRequest.addValue($0.value, forHTTPHeaderField: $0.key) }
// Encode URL Request with the parameters
if encoding != nil {
return try encoding!.encode(urlRequest, with: route.parameters)
} else {
return urlRequest
}
}
func requestAndDecode(completion: #escaping (Result?) -> Void) {
NetworkAdapter.sessionManager.request(urlRequest).validate().responseData { response in
let responseObject = try? JSONDecoder().decode(ApiResponse<self.responseResultType!>, from: response.data!)
completion(responseObject.result)
}
}
}
But in my requestAndDecode method It throws an compiler error (Cannot invoke 'decode' with an argument list of type '(Any.Type, from: Data)'). I can't use ApiResponse<self.responseResultType!> like that.
I could make this function generic and call it like this:
TestRouter.getTestData(byId: 123).requestAndDecode(TestData.self, completion:)
but then I'd have to pass the response type everytime I want to use this endpoint.
What I want to achieve is that the extension function requestAndDecode takes it response type information from itself, the responseResultType property.
Is this possible?
Ignoring the actual error report you have a fundamental problem with requestAndDecode: it is a generic function whose type parameters are determined at the call site which is declared to return a value of type Result yet it attempts to return a value of type self.responseResultType whose value is an unknown type.
If Swift's type system supported this it would require runtime type checking, potential failure, and your code would have to handle that. E.g. you could pass TestData to requestAndDecode while responseResultType might be ValidationResponse...
Change the JSON call to:
JSONDecoder().decode(ApiResponse<Result>.self ...
and the types statically match (even though the actual type that Result is is unknown).
You need to rethink your design. HTH
Create a Generic function with Combine and AlomFire. You can use it for all method(get, post, put, delete)
func fatchData<T: Codable>(requestType: String, url: String, params: [String : Any]?, myType: T.Type, completion: #escaping (Result<T, Error>) -> Void) {
var method = HTTPMethod.get
switch requestType {
case "Get":
method = HTTPMethod.get
case "Post":
method = HTTPMethod.post
print("requestType \(requestType) \(method) ")
case "Put":
method = HTTPMethod.put
default:
method = HTTPMethod.delete
}
print("url \(url) \(method) \(AppConstant.headers) ")
task = AF.request(url, method: method, parameters: params, encoding: JSONEncoding.default, headers: AppConstant.headers)
.publishDecodable(type: myType.self)
.sink(receiveCompletion: { (completion) in
switch completion{
case .finished:
()
case .failure(let error):
// completion(.failure(error))
print("error \(error)")
}
}, receiveValue: {
[weak self ](response) in
print("response \(response)")
switch response.result{
case .success(let model):
completion(.success(model))
print("error success")
case .failure(let error):
completion(.failure(error))
print("error failure \(error.localizedDescription)")
}
}
)
}
How can I get status code from the Alamofire response?
In the latest version I can use validate but I need to check what the status code is.
Code:
Alamofire.request(.GET, "http://example.com/url")
.responseJSON { response in
}
This works (after #MirzaDelic pointed out my mistake):
Alamofire.request(.get, "http://www.google.com")
.responseJSON { response in
if response.response.statusCode == 404 {
// do something
} else {
// do something else
}
}
In Alamofire 4.0 swift3 branch, I needed exactly:
Alamofire.request(request, withMethod: .get, parameters: parameters, encoding: .url, headers: nil)
.validate()
.responseJSON { response in
switch response.result {
case .success:
let statusCode = (response.response?.statusCode)!
print("...HTTP code: \(statusCode)")
}
}
While the above methods will work, there is a more efficient way to do this. response.response.statusCode returns a status code that could very in large amounts. Alamofire includes two functions that are useful for this case, isSuccess and isFailure.
Alamofire.request(.get, "http://example.com/url").responseJSON { response in
let responseErrorCode = response.response!.statusCode
if response.result.isSuccess {
print("Successful HTTP code: \(responseErrorCode)")
// Run code if request is successful
} else if response.result.isFailure {
print("Failure HTTP code: \(responseErrorCode)")
// Run code if request isn't successful
}
}
By using the code above you can easily know whether the HTTP code is good or bad.
With Alamofire 5.6, if you add .validate(statusCode: [200]) to your AF request you can then inspect the AFError of the result.
func request(_ requestConvertible: any URLRequestConvertible) async throws {
switch await AF
.request(requestConvertible, interceptor: interceptor)
.validate(statusCode: [200])
.serializingDecodable(Empty.self, emptyResponseCodes: [200])
.result {
case .failure(let error):
if error.responseCode == 401 {
// Handle 401 statusCode
}
throw error
case .success:
break
}
}
Or for a Decodable type (from a JSON response):
func requestDecodable<Value: Decodable>(_ requestConvertible: any URLRequestConvertible, to type: Value.Type) async throws -> Value {
let request = AF.request(requestConvertible, interceptor: interceptor)
.validate(statusCode: [200])
let response = request.serializingData(emptyResponseCodes: [200])
let result = await response.result
switch result {
case .failure(let error):
if request.response?.statusCode == 401 {
// Handle 401 statusCode
}
throw error
case .success(let data):
return try JSONDecoder().decode(type.self, from: data)
}
}