I want a diffrerent responseDecodable on the httpStatusCode
server return
if statusCode == 200
resonseBody
{id: number}
if statusCode 400..<500
resonseBody
{
code: String
timestamp: String
message: String
}
so now my code is
AF.request(url, method: .post, headers: header).responseData { response in
switch response.result {
case .success(let data) :
guard let response = response.response else {return}
let json = try? JSONSerialization.jsonObject(with: data)
switch response.statusCode {
case 200:
if let json = json as? [String: Any] , let message = json["id"] as? Int{print(message)}
case (400..<500):
if let json = json as? [String: Any] , let message = json["message"] as? String{print(message)}
default:
return
}
case .failure(let err) :
print(err)
}
}
I try this code convert responseDecodable
struct a: Codable {var id: Int}
struct b: Codable{
var code: String
var timestamp: String
var message: String
}
AF.request(url, method: .post, headers: header).responseDecodable(of: a.self) { response in
guard let data = response.value else {return}
print(data)
}
.responseDecodable(of: b.self) { response in
guard let data = response.value else {return}
print(data)
}
but this way Regardless statusCode return both a and b
I want
stautsCode == 200 return a or
statusCode 400..<500 return b
What should I Do?
AFAIK, Alamofire does not have a “decode one object for success and another for failure” implementation. You'll have to do this yourself.
If you really want a one distinct object for 2xx responses and another for 4xx responses, there are a few approaches:
Use validate to handle the 2xx responses, and manually decode 4xx responses in the error handler.
AF.request(url, method: .post, parameters: parameters, headers: header)
.validate(statusCode: 200 ..< 300) // only 2xx are auto-decoded for us; handle 4xx responses in `failure` handler
.responseDecodable(of: Foo.self) { response in
switch response.result {
case .failure(let error):
guard
let statusCode = response.response?.statusCode,
400 ..< 500 ~= statusCode,
let data = response.data,
let apiError = try? JSONDecoder().decode(ApiErrorResponse.self, from: data)
else {
print("other error:", error) // didn't parse `ApiErrorResponse` object, so obviously some other error
return
}
print("apiError:", apiError) // this is our parsed API error object
case .success(let foo):
print("success:", foo)
}
}
If you want, you could write your own ResponseSerializer to parse 2xx and 4xx responses differently:
struct ApiErrorResponse: Decodable, Error {
let code: String
let timestamp: String
let message: String
}
final class ApiResponseSerializer<T: Decodable>: ResponseSerializer {
lazy var decoder = JSONDecoder()
private lazy var successSerializer = DecodableResponseSerializer<T>(decoder: decoder)
private lazy var errorSerializer = DecodableResponseSerializer<ApiErrorResponse>(decoder: decoder)
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T {
if let error = error { throw error }
guard let response = response else { throw URLError(.badServerResponse) }
switch response.statusCode {
case 400 ..< 500:
let apiErrorObject = try errorSerializer.serialize(request: request, response: response, data: data, error: nil)
throw apiErrorObject
default:
return try successSerializer.serialize(request: request, response: response, data: data, error: nil)
}
}
}
And then you can do:
AF.request(url, method: .post, parameters: parameters, headers: header)
.response(responseSerializer: ApiResponseSerializer<Foo>()) { response in
switch response.result {
case .failure(.responseSerializationFailed(reason: .customSerializationFailed(error: let apiErrorObject))):
print("api response error:", apiErrorObject)
case .failure(let error):
print("some other error:", error)
case .success(let foo):
print("success:", foo)
}
}
I find the nesting of the parsed error object to be a little tedious, but it does abstract the 2xx vs 4xx logic out of the response/responseDecoder completion handler.
In both of these, I'm considering all 2xx responses as success, not just 200. Some servers return 2xx codes other than just 200.
Related
In my quest to implement Alamofire 5 correctly and handle custom error model responses, I have yet to find an accepted answer that has an example.
To be as thorough as possible, here is my apiclient
class APIClient {
static let sessionManager: Session = {
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = 30
configuration.waitsForConnectivity = true
return Session(configuration: configuration, eventMonitors: [APILogger()])
}()
#discardableResult
private static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:#escaping (Result<T, AFError>)->Void) -> DataRequest {
return sessionManager.request(route)
// .validate(statusCode: 200..<300) // This will kill the server side error response...
.responseDecodable (decoder: decoder){ (response: DataResponse<T, AFError>) in
completion(response.result)
}
}
static func login(username: String, password: String, completion:#escaping (Result<User, AFError>)->Void) {
performRequest(route: APIRouter.login(username: username, password: password), completion: completion)
}
}
I am using it like this
APIClient.login(username: "", password: "") { result in
debugPrint(result)
switch result {
case .success(let user):
debugPrint("__________SUCCESS__________")
case .failure(let error):
debugPrint("__________FAILURE__________")
debugPrint(error.localizedDescription)
}
}
I have noticed that if I use .validate() that the calling function will receive a failure however the response data is missing. Looking around it was noted here and here to cast underlyingError but thats nil.
The server responds with a parsable error model that I need at the calling function level. It would be far more pleasant to deserialize the JSON at the apiclient level and return it back to the calling function as a failure.
{
"errorObject": {
"summary": "",
"details": [{
...
}]
}
}
UPDATE
Thanks to #GIJoeCodes comment I implemented this similar solution using the router.
class APIClient {
static let sessionManager: Session = {
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = 30
configuration.waitsForConnectivity = true
return Session(configuration: configuration, eventMonitors: [APILogger()])
}()
#discardableResult
private static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:#escaping (_ response: T?, _ error: Error?)->Void) {
sessionManager.request(route)
.validate(statusCode: 200..<300) // This will kill the server side error response...
.validate(contentType: ["application/json"])
.responseJSON { response in
guard let data = response.data else { return }
do {
switch response.result {
case .success:
let object = try decoder.decode(T.self, from: data)
completion(object, nil)
case .failure:
let error = try decoder.decode(ErrorWrapper.self, from: data)
completion(nil, error.error)
}
} catch {
debugPrint(error)
}
}
}
// MARK: - Authentication
static func login(username: String, password: String, completion:#escaping (_ response: User?, _ error: Error?)->Void) {
performRequest(route: APIRouter.login(username: username, password: password), completion: completion)
}
}
Called like this
APIClient.login(username: "", password: "") { (user, error) in
if let error = error {
debugPrint("__________FAILURE__________")
debugPrint(error)
return
}
if let user = user {
debugPrint("__________SUCCESS__________")
debugPrint(user)
}
}
This is how I get the errors and customize my error messages. In the validation, I get the errors outside of the 200..<300 response:
AF.request(
url,
method: .post,
parameters: json,
encoder: JSONParameterEncoder.prettyPrinted,
headers: headers
).validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseJSON { response in
switch response.result {
case .success(let result):
let json = JSON(result)
onSuccess()
case .failure(let error):
guard let data = response.data else { return }
do {
let json = try JSON(data: data)
let message = json["message"]
onError(message.rawValue as! String)
} catch {
print(error)
}
onError(error.localizedDescription)
}
debugPrint(response)
}
First, there's no need to use responseJSON if you already have a Decodable model. You're doing unnecessary work by decoding the response data multiple times. Use responseDecodable and provide your Decodable type, in this case your generic T. responseDecodable(of: T).
Second, wrapping your expected Decodable types in an enum is a typical approach to solving this problem. For instance:
enum APIResponse<T: Decodable> {
case success(T)
case failure(APIError)
}
Then implement APIResponse's Decodable to try to parse either the successful type or APIError (there are a lot of examples of this). You can then parse your response using responseDecodable(of: APIResponse<T>.self).
I am facing an issue with Alamofire requests and interceptor.
I have two types of responses from my server -> Success response which is kind of object and the error response which is always a message what's wrong.
I founded a sample code to create a very own function for decoding two types of responses which seems to work well.
The problem occurs after the Alamofire adapt function is called (adapt add a cookies for each request) -> in my .responseTwoDecodable i'll get always response as nil but the status code is 200 and everything is fine, server returns object but alamofire is ignoring it.
Here is my code for each request:
func request<T: Decodable>(
_ url: String,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
decoder: JSONDecoder = JSONDecoder(),
headers: HTTPHeaders? = nil,
interceptor: RequestInterceptor? = nil
) -> Future<T, ServerError> {
return Future({ promise in
AF.request(
url,
method: method,
parameters: parameters,
encoding: JSONEncoding.default,
headers: headers
interceptor: interceptor ?? self
)
.validate(statusCode: [200, 201, 204, 401]
.responseTwoDecodable(of: T.self) { response in
switch response {
case .success(let value):
promise(.success(value))
case .failure(let error):
promise(.failure(error))
}
}
})
}
Here is adapt function:
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, ServerError>) -> Void) {
let request = urlRequest
if let accessToken = UserDefaults.standard.string(forKey: Constants.accessToken), let refreshToken = UserDefaults.standard.string(forKey: Constants.refreshToken) {
let access = [
HTTPCookiePropertyKey.domain: Constants.cookieDomain,
HTTPCookiePropertyKey.path: Constants.cookieAccessPath,
HTTPCookiePropertyKey.name: Constants.accessToken,
HTTPCookiePropertyKey.value: accessToken
]
let refresh = [
HTTPCookiePropertyKey.domain: Constants.cookieDomain,
HTTPCookiePropertyKey.path: Constants.cookieRefreshPath,
HTTPCookiePropertyKey.name: Constants.refreshToken,
HTTPCookiePropertyKey.value: refreshToken
]
if let accessCookie = HTTPCookie(properties: access), let refreshCookie = HTTPCookie(properties: refresh) {
AF.session.configuration.httpCookieStorage?.setCookie(accessCookie)
AF.session.configuration.httpCookieStorage?.setCookie(refreshCookie)
completion(.success(request))
}
}
completion(.success(request))
}
And here is my decoding code for two decodables:
struct ErrorMessage: Error, Decodable {
let message: String
}
struct ServerError: Error {
var message: String
var code: ServerErrorCodes
let args: [String]?
}
enum ServerErrorCodes: Int {
case unauthorized = 401
case forbidden = 403
case internalServerError = 500
case notFound = 404
case conflict = 409
case unknown
case emptyResponse
case unserialized
case userInput
case coreData
}
final class TwoDecodableResponseSerializer<T: Decodable>: ResponseSerializer {
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
private lazy var successSerializer = DecodableResponseSerializer<T>(decoder: decoder)
private lazy var errorSerializer = DecodableResponseSerializer<ErrorMessage>(decoder: decoder)
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Result<T, ServerError> {
guard error == nil else { return .failure(ServerError(message: "Unknown error", code: .unknown, args: [])) }
guard let response = response else { return .failure(ServerError(message: "Empty response", code: .emptyResponse, args: [])) } // HERE I AM GETTING A NIL AS A RESPONSE, BUT SERVER RESPONDED WITH CORRECT BODY AND 200 AS STATUS CODE
do {
print(response.debugDescription)
if response.statusCode < 200 || response.statusCode >= 300 {
let serverMessage = try errorSerializer.serialize(request: request, response: response, data: data, error: nil)
let responseCode: ServerErrorCodes
if let code = ServerErrorCodes(rawValue: response.statusCode) {
responseCode = code
} else {
responseCode = .unknown
}
let result = ServerError(message: serverMessage.message, code: responseCode, args: nil)
return .failure(result)
} else {
let result = try successSerializer.serialize(request: request, response: response, data: data, error: nil)
return .success(result)
}
} catch(let err) {
return .failure(ServerError(message: "Could not serialize body", code: .unserialized, args: [String(data: data!, encoding: .utf8)!, err.localizedDescription]))
}
}
}
extension DataRequest {
#discardableResult func responseTwoDecodable<T: Decodable>(queue: DispatchQueue = DispatchQueue.global(qos: .userInitiated), of t: T.Type, completionHandler: #escaping (Result<T, ServerError>) -> Void) -> Self {
return response(queue: .main, responseSerializer: TwoDecodableResponseSerializer<T>()) { response in
switch response.result {
case .success(let result):
completionHandler(result)
case .failure(let error):
completionHandler(.failure(ServerError(message: "Other error", code: .unknown, args: [error.localizedDescription])))
}
}
}
}
I am a newbie in Alamofire, so if something can be done in better way, I will appreciate your help or sharing your thoughts! Has anybody idea why this can happen? Thanks!
SOLVED: I completely wipe out the code implementing .responseTwoDecodable and write very own solution:
func createError(response: HTTPURLResponse?, error: AFError, data: Data?) -> ServerError {
guard let serverResponse = response else {
return .init(message: "Unknown error", code: .unknown, args: [error.localizedDescription])
}
guard let serverData = data else {
return .init(message: "Empty response", code: .emptyResponse, args: [error.localizedDescription])
}
let responseCode: ServerErrorCodes
if let code = ServerErrorCodes(rawValue: serverResponse.statusCode) {
responseCode = code
} else {
responseCode = .unknown
}
do {
let serverMessage = try JSONDecoder().decode(ErrorMessage.self, from: serverData)
return .init(message: serverMessage.message, code: responseCode, args: [])
} catch (let error) {
return .init(message: "Body not serializable", code: .unserialized, args: [String(data: serverData, encoding: .utf8) ?? "", error.localizedDescription])
}
}
And calling it like:
.responseDecodable(completionHandler: { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
promise(.success(value))
case .failure(let error):
promise(.failure(self.createError(response: response.response, error: error, data: response.data)))
}
})
With this code I was able to decode all the errors from the server as expected.
I am trying to call API in postman and its working fine, But If I am trying to call API in swift Alamofire, Its give me error-
My Code is-
func sendToServer(){
let urlString = "https://xxxxxxxxxx/TempService/SaveBarCodes"
let data: Parameters = ["SerialNo": "T8180399","Status":101]
Alamofire.request(urlString, method: .post, parameters: data,encoding: JSONEncoding.default, headers: nil).responseJSON {
response in
switch response.result {
case .success:
print(response)
break
case .failure(let error):
print(error)
}
}
}
Error is-
The JSON value could not be converted to System.Collections.Generic.List`1[BreakBulkModels.Model.WebapiModels.DtoInventoryApi]. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
Your API accepts as parameters an array of JSON objects but you are currently sending a JSON object:
{
"SerialNo": "T8180399",
"Status": 101
}
Because Parameters is a typealias to Dictionary<String, Any> (what you need is Array<Dictionary<String, Any>>) you have to do your parameter encoding yourself and then call request(_:) function of Alamofire passing your URLRequest:
do {
let urlString = "https://xxxxxxxxxx/TempService/SaveBarCodes"
let url = try urlString.asURL()
var request = URLRequest(url: url)
let data = [["SerialNo": "T8180399", "Status": 101]]
request = try JSONEncoding.default.encode(request, withJSONObject: data)
Alamofire.request(request).responseJSON { response in
switch response.result {
case .success:
print(response)
break
case .failure(let error):
print(error)
}
}
} catch {
print(error)
}
Edit: With Alamofire v5 there is a more elegant way by using Encodable protocol:
struct BarCode: Encodable {
var SerialNo: String
var Status: Int
}
func sendToServer(){
let urlString = "https://xxxxxxxxxx/TempService/SaveBarCodes"
let data = [BarCode(SerialNo: "T8180399", Status: 101)]
AF.request(
urlString,
method: .post,
parameters: data,
encoder: JSONParameterEncoder.default
).responseJSON { response in
switch response.result {
case .success:
print(response)
break
case .failure(let error):
print(error)
}
}
}
I am having trouble populating a UITextField with my returnHTML data that I get from my web-service.
If I have my web-service such that:
import Foundation;
class WebSessionCredentials {
static let requestURL = URL(string:"xxxx.on.ca/getData.aspx?requestType=Tech")!
var htmlbody: String?
var instancedTask: URLSessionDataTask?
static var sharedInstance = WebSessionCredentials()
init() {
self.instancedTask = URLSession.shared.dataTask(with: WebSessionCredentials.requestURL) { [weak self] (data,response,error) in
if let error = error {
// Error
print("Client Error: \(error.localizedDescription)")
return
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
print("Server Error!")
return
}
guard let mime = response.mimeType, mime == "text/html" else {
print("Wrong mime type!");
return
}
if let htmlData = data, let htmlBodyString = String(data: htmlData, encoding: .utf8) {
self?.htmlbody = htmlBodyString;
};
};
};
};
Through this I should be able to access the returned HTML response through WebSessionCredentials.sharedInstance.htmlbody;
Verifying this in playground I seem to be getting the correct response within the class but when calling htmlbody from outside the class I get a nil response - I am out of ideas in terms of how to send that HTML string that I get from the class to outside the function. This question is built off another question I have posted a couple days earlier -> Delegating privately declared variables to a public scope
Thanks,
Rather than implementing the dataTask in the init method add a method run with completion handler
class WebSessionCredentials {
enum WebSessionError : Error {
case badResponse(String)
}
static let requestURL = URL(string:"xxxx.on.ca/getData.aspx?requestType=Tech")!
static var sharedInstance = WebSessionCredentials()
func run(completion : #escaping (Result<String,Error>) -> Void) {
let instancedTask = URLSession.shared.dataTask(with: WebSessionCredentials.requestURL) { (data,response,error) in
if let error = error {
// Error
print("Client Error: \(error.localizedDescription)")
completion(.failure(error))
return
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
completion(.failure(WebSessionError.badResponse("Server Error!")))
return
}
guard let mime = response.mimeType, mime == "text/html" else {
completion(.failure(WebSessionError.badResponse("Wrong mime type!")))
return
}
completion(.success(String(data: data!, encoding: .utf8)!))
}
instancedTask.resume()
}
}
And use it
WebSessionCredentials.sharedInstance.run { result in
switch result {
case .success(let htmlBody): print(htmlBody)
case .failure(let error): print(error)
}
}
I am using generics and codable with URLSession.
When I receive a response from an API, I check the status is in the 200 - 299 range and decode the data like so
guard let data = data, let value = try? JSONDecoder().decode(T.self, from: data) else {
return completion(.error("Could not decode JSON response"))
}
completion(.success(value))
This is then passed off to the completion handler and everything is OK.
I have a new endpoint I must POST too however, this endpoint returns a 204 with no content body.
As such, I cannot decode the response, simply as I cannot pass in a type?
My completion handler expects
enum Either<T> {
case success(T)
case error(String?)
}
and switching on my response statusCode like so
case 204:
let value = String(stringLiteral: "no content")
return completion(.success(value))
produces an error of
Member 'success' in 'Either<>' produces result of type 'Either',
but context expects 'Either<>'
My APIClient is
protocol APIClientProtocol: class {
var task: URLSessionDataTask { get set }
var session: SessionProtocol { get }
func call<T: Codable>(with request: URLRequest, completion: #escaping (Either<T>) -> Void) -> Void
func requestRefreshToken<T: Codable>(forRequest failedRequest: URLRequest, completion: #escaping (Either<T>) -> Void) -> Void
}
extension APIClientProtocol {
func call<T: Codable>(with request: URLRequest, completion: #escaping (Either<T>) -> Void) -> Void {
task = session.dataTask(with: request, completionHandler: { [weak self] data, response, error in
guard error == nil else {
return completion(.error("An unknown error occured with the remote service"))
}
guard let response = response as? HTTPURLResponse else {
return completion(.error("Invalid HTTPURLResponse recieved"))
}
switch response.statusCode {
case 100...299:
guard let data = data else { return completion(.error("No data in response")) }
guard let value = try? JSONDecoder().decode(T.self, from: data) else { return completion(.error("Could not decode the JSON response")) }
completion(.success(value))
case 300...399: return completion(.error(HTTPResponseStatus.redirection.rawValue))
case 400: return completion(.error(HTTPResponseStatus.clientError.rawValue))
case 401: self?.requestRefreshToken(forRequest: request, completion: completion)
case 402...499: return completion(.error(HTTPResponseStatus.clientError.rawValue))
case 500...599: return completion(.error(HTTPResponseStatus.serverError.rawValue))
default:
return completion(.error("Request failed with a status code of \(response.statusCode)"))
}
})
task.resume()
}
}
Make your Either enum success type optional
enum Either<T> {
case success(T?)
case error(String)
}
Create a case for a 204 response status, passing nil
case 204:
completion(.success(nil))
You can then use a typealias and create something generic like
typealias NoContentResponse = Either<Bool>
You should then be able to switch on NoContentResponse.
case 200...299: //success status code
if data.isEmpty {
let dummyData = "{}".data(using: .utf8)
//Use empty model
model = try JSONDecoder().decode(T, from: dummyData!)
}