cast server response in AFError - swift

What I'm trying to achieve is that I have a NetworkManager that handles the request's to the server, and handle the error through AFError.
However sometimes when the server response is 4xx, there is a custom message with that response which I want to show that to the user But don't know how to implement it.
This is my NetworkManager
static let shared:NetworkManager = {
return NetworkManager()
}()
typealias completionHandler = ((Result<Data, AFError>) ->Void)
func handleAFrequest(request: DataRequest,completion: #escaping completionHandler) {
request.validate(statusCode: 200..<300)
request.responseJSON { (response) in
switch response.result {
case .success(_):
if let data = response.data {
completion(.success(data))
}
case .failure(let error):
print(error.localizedDescription)
switch error {
case .invalidURL(let url):
print("Invalid URL: \(url) - \(error.localizedDescription)")
completion(.failure(.invalidURL(url: URL)))
case .responseValidationFailed(let reason):
print("Response validation failed: \(error.localizedDescription); Reason:\(reason)")
completion(.failure(.responseValidationFailed(reason: reason)))
I want to be able to cast server response in addition to the error, and show Message of the response to the user.
Server Response example when StatusCode is 4xx:
{
"data":
"code":401;
"Message":"Phone Invalid"
}

I have parsed api errors in many of my projects. I believe there is a better alternative to handle the showing or errors if any, to the user. Please see my code, in it, if there is a error I show it in a toast. Showing in a toast is the not focal point but you can see how I handle the error case in my code and it has never failed. Please change the params accordingly to your api call
func postMethod(mylist: [String:Any]) {
print(K.APIUrl)
print(K.port)
AF.request("\(K.urlFromUrlField!):\(K.configPort)/snhttp-01?", method: .put, parameters: mylist)
.authenticate(username: username, password: password)
.response { response in
switch response.result {
case .success:
print("\nValidation Successful from put method")
print(response.result)
print(response.value as Any)
//get xml code and error msg if any
if let response = response.data{
let xml = XML.parse(response)
print(xml)
print("\nThis is the data sent to the server: \(mylist["data"] ?? "No data in data key of the parameter")" )
let code = xml.Response.Code.text ?? "No code value in response"
let responseMessage = xml.Response.Message.text ?? "No message returned from server"
print("\nCode value from server: \(code)")
print("\nResponse message from server: \(responseMessage)")
}
else{
print("\nSuccess block: Request Successfully sent, BUT there was nothing from the server to unwrap! / nothing sent back from the server\nThis is the data sent to the server: \(mylist["data"] ?? "No data in data key of the parameter")")
}
case let .failure(error):
if let response = response.data {
let xml = XML.parse(response)
let code = xml.Response.Code.text ?? "\nNo code value in response"
let responseMessage = xml.Response.Message.text ?? "No message returned from server"
print("\nCode value from server: \(code)")
print("\nResponse message from server: \(responseMessage)")
print(error)
}
else {
print("\nFailure Block: A connection to the server could not be established")
}
}}
}
This code parses the xml from the api. However you can discard that and just focus on how I handle the response and consequently the error.

This is the solution that works for me.
All you need to do is create a custom error type:
struct APIError: Error, Decodable {
let status: Int?
let message: String?
let key: String?
}
Then call Alamofire, which will return an AFDataResponse which you can parse:
func performRequest<T: Decodable>(route: APIRouter, completion: #escaping (APIResponse<T>) -> Void) {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
AF.request(route)
.validate()
.responseDecodable(decoder: decoder, emptyResponseCodes: [200, 201]) { (response: AFDataResponse<T>) in
self.parseResponse(response: response, completion: completion)
}
}
Parsing is done like this:
private func parseResponse<T: Decodable>(response: AFDataResponse<T>, completion: #escaping (APIResponse<T>) -> Void) {
switch response.result {
case .success(let data):
completion(APIResponse<T>.success(data))
case .failure(let error):
if let data = response.data,
// THIS IS WHERE YOU CAST AFError TO YOUR CUSTOM APIError
let apiError = try? JSONDecoder().decode(APIError.self, from: data) {
completion(APIResponse.failure(apiError))
} else {
completion(APIResponse.failure(error))
}
}
}
Hope this helps!

Related

Making multiple requests with Alamofire depending on array of chunks

I'm struggling with making badge requests with Alamofire and I need help.
I have some ids and with them I need to struct parameters (Dictionary String) and send a GET request with Alamofire. Everything is fine, but I need to cover the case when ids are above 200, because when they are more than 200, API returns 414 code status (too long URL). So when ids are more than 200 they are separated in chunks. With each chunk I'm making a new request to API. The problem is that I return only the first 200 ids when I call my method. Here is an example:
func request (_ idsDict: [String: [String]], _ idSchema: String, _ completion: #escaping Result<SomeModel, Error>) -> Void {
let chunks = transformEntitiesIdsToChunks(idsDict)
let decoder = JSONDecoder()
chunks.forEach {chunk in
let parameters = constructQueryParams(idsDict, chunk, idSchema, apiKey, clientId)
AF.request(baseURL, parameters: parameters).response { response in
switch response.result {
case .success(let data):
// some error handling for decoding and no data
completion(.success(data.data))
case .failure(let error):
return completion(.failure(error.localizedDescription))
}
}
}
}
// Method wraps AF request in a continuation block and makes sure that the closure from request method returned data or throwed error.
// That way fetching from API becomes async/await and can be used in do/try/catch block.
func getIdsEntities (_ idsDict: [String: [String]], _ idSchema: String) async throws -> [SomeModel] {
return try await withUnsafeThrowingContinuation { continuation in
request(idsDict, idSchema) { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
return
case .failure(let error):
continuation.resume(throwing: error)
return
}
}
}
}
I have tried with recursive functions and with DispatchGroup but none of them worked. Any help will be appriciated. Thank you in advance.
Thanks to Larme's comment I was able to find my mistake. When making request to API I was passing the decoded response to the completion closure. To fix this I had to declare an array of model let responses:[SomeModel] = [] and append the decoded result to it. I used let group = DispatchGroup() so I can wait the requests to execute and have my final array of results and then I used group.notify(queue: .main, execute: {completion(.success(responses))}) to return to the main queue and have my array of completed fetched data. This is now how my code looks like:
private func request (_ idsDict: [String: [String]], _ idSchema: String, _ completion: #escaping APIListResponseClosure<SomeModel>) -> Void {
var responses: [SomeModel] = []
let group = DispatchGroup()
let chunks = transformEntitiesIdsToChunks(idsDict)
let decoder = JSONDecoder()
chunks.forEach {chunk in
group.enter()
let parameters = constructQueryParams(idsDict, chunk, idSchema, apiKey, clientId)
AF.request(baseURL, parameters: parameters).response { response in
switch response.result {
case .success(let data):
// some error handling for decoding and no data
responses.append(data.data)
group.leave()
case .failure(let error):
return completion(.failure(.APIError(error.localizedDescription)))
}
}
}
group.notify(queue: .main, execute: {
print("Ids are fetched")
completion(.success(responses))
})
}
Thanks again to Larme and I hope I helped someone else with this case.

How do I read the property values of a JSON error object using Combine in Swift?

All of my API endpoints return a response which looks something like this in Postman:
{
"statusCode": 401,
"error": "Unauthorized",
"message": "Missing authentication"
}
What I would like to do is make the request, and have access to these properties in Swift. There will be some cases where I use the error message property's value in the front of the app. This will be determined by the statusCode returned.
What I have right now is this:
private var cancellable: AnyCancellable?
let url = URL(string: "http://abc336699.com/create")
self.cancellable = URLSession.shared.dataTaskPublisher(for: url!)
.map { $0.data }
Prior to this, I tried tryMap, but the type of error it returned didn't give me the flexibility I wanted. I then moved on and tried Almofire, but it seemed like an over kill for what I want to do.
I wanted to check what is being returned in the response I get, but I get the following error:
Cannot assign value of type 'Publishers.Map<URLSession.DataTaskPublisher, Data>' to type 'AnyCancellable'
I want simple access to my response errors so I can integrate the API throughout the app using combine.
I am not sure from where you will be getting your data as in JSON response there is no Key for data. Before writing below code my understanding was that you want to check error and statusCode from the mentioned JSON response and then move forward with your business logic. The below code is to give you a vague idea of how we can do that.
enum CustomError: Error {
case custom(_ error: String)
case unknownStatusCode
case errorOccurred
}
let url = URL(string: "http://abc336699.com/create")
func load() -> AnyPublisher<Data,CustomError> {
URLSession.shared.dataTaskPublisher(for: url!)
.map(\.data)
.tryMap { (data) -> Data in
let genericModel = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: AnyObject]
if let statusCode = genericModel?["statusCode"] as? String {
switch statusCode {
case "200":
guard let data = genericModel?["message"] as? Data else {
throw CustomError.custom("Parsing error")
}
return data
default:
if let error = genericModel?["error"] as? String {
throw CustomError.custom(error)
} else {
throw CustomError.unknownError
}
}
}
throw CustomError.errorOccurred
}
.decode(type: YourCustomDecodableModel.self, decoder: JSONDecoder())
.mapError({ $0 as? CustomError ?? CustomError.errorOccurred })
.eraseToAnyPublisher()
}

Cannot get result while I use Alamofire

I am using Alamofire version 3.1.5 with my xCode version 7.3. I tried it on many apps of mine. But I do not get any result. No error is seen before the execution. My code is like this:
var apiUrl : String
var alamoFireManager:Manager!
var configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
init(apiUrl : String){
self.apiUrl = apiUrl
configuration.timeoutIntervalForRequest = 30.0
alamoFireManager = Alamofire.Manager(configuration: configuration)
}
print(apiUrl)
alamoFireManager.request(.GET, apiUrl, encoding: .JSON).validate(contentType: ["application/json"]) .responseJSON { response in
print(response.result.value)
switch response.result{
case .Success:
if let value = response.result.value{
let json = JSON(value)
print (json)
callback.onResponse(value) // this is where my value is sent
}
case .Failure(let error):
print(error.localizedDescription)
}
}
I checked using breakpoint and the app directly reaches this last braces frm the alamofire line.
I think the issue is you are not calling resume(). From the Alamofire doc
If the owning manager does not have startRequestsImmediately set to
true, the request must call resume() in order to start.
try this
alamoFireManager.request(.GET, apiUrl, encoding: .json).validate(contentType: ["application/json"]).resume().responseJSON { response in
print(response.result.value)
switch response.result{
case .Success:
if let value = response.result.value{
let json = JSON(value)
print (json)
callback.onResponse(value) // this is where my value is sent
}
case .Failure(let error):
print(error.localizedDescription)
}
}

Swift catching error

Why is
if error = response.result.error else {
print (error)
}
Not allowed and throws an error within the function below? I would have expected it should go through well.
Alamofire.request(Router.RegisterUser(parameters))
.validate()
.responseJSON(completionHandler: { (response) -> Void in
if let receivedData = response.result.value {
let json = JSON(receivedData)
// Check for error in JSON
if let message = json["error"]["message"].string {
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: message)
print(error)
return
}
completionHandler(.Success(true), response: JSON(receivedData))
}
if error = response.result.error else {
print (error)
}
})
I use the validate function to avoid successful responses to the completionhandler, when indeed an error code is returned by the server. The problem now is, that the only error I can currently receive is the one, thrown by validate(). But I use error handling in my API as well and will return a suitable error message to the user.
Error Domain=com.alamofire.error Code=-6003 "Response status code was
unacceptable: 400" UserInfo={NSLocalizedFailureReason=Response status
code was unacceptable: 400}
How can I achieve this now? As by using validate, the response will never get processed as far as I understand?
Alamofire.request(Router.RegisterUser(parameters))
.validate()
.responseJSON(completionHandler: { (response) -> Void in
switch(response.result) {
case .Success(let response):
print(response)
case .Failure(let error):
print("FAILURE: \(error)")
}
})
}
There seems to be 2 issues in your code:
You are missing a let or guard to check if there is an error.
print(error) should be inside if as you are checking error is not nil and trying to print error in else which will be nil.

Swift Alamofire: How to get the HTTP response status code

I would like to retrieve the HTTP response status code (e.g. 400, 401, 403, 503, etc) for request failures (and ideally for successes too). In this code, I am performing user authentication with HTTP Basic and want to be able to message the user that authentication failed when the user mistypes their password.
Alamofire.request(.GET, "https://host.com/a/path").authenticate(user: "user", password: "typo")
.responseString { (req, res, data, error) in
if error != nil {
println("STRING Error:: error:\(error)")
println(" req:\(req)")
println(" res:\(res)")
println(" data:\(data)")
return
}
println("SUCCESS for String")
}
.responseJSON { (req, res, data, error) in
if error != nil {
println("JSON Error:: error:\(error)")
println(" req:\(req)")
println(" res:\(res)")
println(" data:\(data)")
return
}
println("SUCCESS for JSON")
}
Unfortunately, the error produced does not seem to indicate that an HTTP status code 409 was actually received:
STRING Error:: error:Optional(Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo=0x7f9beb8efce0 {NSErrorFailingURLKey=https://host.com/a/path, NSLocalizedDescription=cancelled, NSErrorFailingURLStringKey=https://host.com/a/path})
req:<NSMutableURLRequest: 0x7f9beb89d5e0> { URL: https://host.com/a/path }
res:nil
data:Optional("")
JSON Error:: error:Optional(Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo=0x7f9beb8efce0 {NSErrorFailingURLKey=https://host.com/a/path, NSLocalizedDescription=cancelled, NSErrorFailingURLStringKey=https://host.com/a/path})
req:<NSMutableURLRequest: 0x7f9beb89d5e0> { URL: https://host.com/a/path }
res:nil
data:nil
Additionally, it would be nice to retrieve the HTTP body when an error occurs because my server-side will put a textual description of the error there.
Questions
Is it possible to retrieve the status code upon a non-2xx response?
Is it possible to retrieve the specific status code upon a 2xx response?
Is it possible to retrieve the HTTP body upon a non-2xx response?
Thanks!
For Swift 3.x / Swift 4.0 / Swift 5.0 users with Alamofire >= 4.0 / Alamofire >= 5.0
response.response?.statusCode
More verbose example:
Alamofire.request(urlString)
.responseString { response in
print("Success: \(response.result.isSuccess)")
print("Response String: \(response.result.value)")
var statusCode = response.response?.statusCode
if let error = response.result.error as? AFError {
statusCode = error._code // statusCode private
switch error {
case .invalidURL(let url):
print("Invalid URL: \(url) - \(error.localizedDescription)")
case .parameterEncodingFailed(let reason):
print("Parameter encoding failed: \(error.localizedDescription)")
print("Failure Reason: \(reason)")
case .multipartEncodingFailed(let reason):
print("Multipart encoding failed: \(error.localizedDescription)")
print("Failure Reason: \(reason)")
case .responseValidationFailed(let reason):
print("Response validation failed: \(error.localizedDescription)")
print("Failure Reason: \(reason)")
switch reason {
case .dataFileNil, .dataFileReadFailed:
print("Downloaded file could not be read")
case .missingContentType(let acceptableContentTypes):
print("Content Type Missing: \(acceptableContentTypes)")
case .unacceptableContentType(let acceptableContentTypes, let responseContentType):
print("Response content type: \(responseContentType) was unacceptable: \(acceptableContentTypes)")
case .unacceptableStatusCode(let code):
print("Response status code was unacceptable: \(code)")
statusCode = code
}
case .responseSerializationFailed(let reason):
print("Response serialization failed: \(error.localizedDescription)")
print("Failure Reason: \(reason)")
// statusCode = 3840 ???? maybe..
default:break
}
print("Underlying error: \(error.underlyingError)")
} else if let error = response.result.error as? URLError {
print("URLError occurred: \(error)")
} else {
print("Unknown error: \(response.result.error)")
}
print(statusCode) // the status code
}
(Alamofire 4 contains a completely new error system, look here for details)
For Swift 2.x users with Alamofire >= 3.0
Alamofire.request(.GET, urlString)
.responseString { response in
print("Success: \(response.result.isSuccess)")
print("Response String: \(response.result.value)")
if let alamoError = response.result.error {
let alamoCode = alamoError.code
let statusCode = (response.response?.statusCode)!
} else { //no errors
let statusCode = (response.response?.statusCode)! //example : 200
}
}
In the completion handler with argument response below I find the http status code is in response.response.statusCode:
Alamofire.request(.POST, urlString, parameters: parameters)
.responseJSON(completionHandler: {response in
switch(response.result) {
case .Success(let JSON):
// Yeah! Hand response
case .Failure(let error):
let message : String
if let httpStatusCode = response.response?.statusCode {
switch(httpStatusCode) {
case 400:
message = "Username or password not provided."
case 401:
message = "Incorrect password for user '\(name)'."
...
}
} else {
message = error.localizedDescription
}
// display alert with error message
}
Alamofire
.request(.GET, "REQUEST_URL", parameters: parms, headers: headers)
.validate(statusCode: 200..<300)
.responseJSON{ response in
switch response.result{
case .Success:
if let JSON = response.result.value
{
}
case .Failure(let error):
}
Best way to get the status code using alamofire.
Alamofire.request(URL).responseJSON {
response in
let status = response.response?.statusCode
print("STATUS \(status)")
}
Or use pattern matching
if let error = response.result.error as? AFError {
if case .responseValidationFailed(.unacceptableStatusCode(let code)) = error {
print(code)
}
}
you may check the following code for status code handler by alamofire
let request = URLRequest(url: URL(string:"url string")!)
Alamofire.request(request).validate(statusCode: 200..<300).responseJSON { (response) in
switch response.result {
case .success(let data as [String:Any]):
completion(true,data)
case .failure(let err):
print(err.localizedDescription)
completion(false,err)
default:
completion(false,nil)
}
}
if status code is not validate it will be enter the failure in switch case
In your responseJSON completion, you can get the status code from the response object, which has a type of NSHTTPURLResponse?:
if let response = res {
var statusCode = response.statusCode
}
This will work regardless of whether the status code is in the error range. For more information, take a look at the NSHTTPURLResponse documentation.
For your other question, you can use the responseString function to get the raw response body. You can add this in addition to responseJSON and both will be called.
.responseJson { (req, res, json, error) in
// existing code
}
.responseString { (_, _, body, _) in
// body is a String? containing the response body
}
Your error indicates that the operation is being cancelled for some reason. I'd need more details to understand why. But I think the bigger issue may be that since your endpoint https://host.com/a/path is bogus, there is no real server response to report, and hence you're seeing nil.
If you hit up a valid endpoint that serves up a proper response, you should see a non-nil value for res (using the techniques Sam mentions) in the form of a NSURLHTTPResponse object with properties like statusCode, etc.
Also, just to be clear, error is of type NSError. It tells you why the network request failed. The status code of the failure on the server side is actually a part of the response.
Hope that helps answer your main question.
I needed to know how to get the actual error code number.
I inherited a project from someone else and I had to get the error codes from a .catch clause that they had previously setup for Alamofire:
} .catch { (error) in
guard let error = error as? AFError else { return }
guard let statusCode = error.responseCode else { return }
print("Alamofire statusCode num is: ", statusCode)
}
Or if you need to get it from the response value follow #mbryzinski's answer
Alamofire ... { (response) in
guard let error = response.result.error as? AFError else { return }
guard let statusCode = error.responseCode else { return }
print("Alamofire statusCode num is: ", statusCode)
})
For Swift 2.0 users with Alamofire > 2.0
Alamofire.request(.GET, url)
.responseString { _, response, result in
if response?.statusCode == 200{
//Do something with result
}
}
For Swift 3.x / Swift 4.0 / Swift 5.0 users with Alamofire >= 5.0
Used request modifier to increase and decrease the timeout interval.
Alamofire's request creation methods offer the most common parameters for customization but sometimes those just aren't enough. The URLRequests created from the passed values can be modified by using a RequestModifier closure when creating requests. For example, to set the URLRequest's timeoutInterval to 120 seconds, modify the request in the closure.
var manager = Session.default
manager.request(urlString, method: method, parameters: dict, headers: headers, requestModifier: { $0.timeoutInterval = 120 }).validate().responseJSON { response in
OR
RequestModifiers also work with trailing closure syntax.
var manager = Session.default
manager.request("https://httpbin.org/get") { urlRequest in
urlRequest.timeoutInterval = 60
urlRequest.allowsConstrainedNetworkAccess = false
}
.response(...)
AF.request(url, method: .get).responseDecodable(of: Weather.self) { response in
switch response.result {
case .success(let data):
print(data)
var statusCode = response.response?.statusCode
if statusCode == 200 {
print(response)
}
case .failure(let error):
print(error)
}
}