Alamofire - How to get API error from AFError - swift

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).

Related

How to decode error response message in Combine?

I'm doing login using SwiftUI and Combine. Could you please give me some idea how can I decode and show json error when user types incorrect email or password? I can only get token.
When I'm doing the same login request with incorrect email or password, server returns me this error message:
{
"code": "[jwt_auth] incorrect_password",
"message": "Incorrect password!",
"data": {
"status": 403
}
}
The problem is that I can't understand how can I decode two different json responses when doing one request in Combine? I can only get token.
Here's model for login request:
struct LoginResponse: Decodable {
let token: String }
struct ErrorResponse: Decodable {
let message: String
}
struct Login: Codable {
let username: String
let password: String
}
static func login(email: String, password: String) -> AnyPublisher<LoginResponse, Error> {
let url = MarketplaceAPI.jwtAuth!
var request = URLRequest(url: url)
let encoder = JSONEncoder()
let login = Login(username: email, password: password)
let jsonData = try? encoder.encode(login)
request.httpBody = jsonData
request.httpMethod = HTTPMethod.POST.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return URLSession.shared
.dataTaskPublisher(for: request)
.print()
.receive(on: DispatchQueue.main)
.map(\.data)
.decode(
type: LoginResponse.self,
decoder: JSONDecoder())
.eraseToAnyPublisher()
}
And in viewModel:
MarketplaceAPI.login(email: email, password: password)
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print("Failure error:", error.localizedDescription) // This's returning token error
}
},
receiveValue: { value in
print("Token:", value.token)
}
})
.store(in: &subscriptions)
}
I would make ErrorResponse conform to the Error protocol:
struct ErrorResponse: Decodable, Error {
let message: String
}
Then, use tryMap instead of decode (which is sort of a special case of tryMap).
.tryMap({ data -> LoginResponse in
let decoder = JSONDecoder()
guard let loginResponse = try? decoder.decode(LoginResponse.self, from: data) else {
throw try decoder.decode(ErrorResponse.self, from: data)
}
return loginResponse
})
First, try to decode the data as a LoginResponse. Note the use of try? here. This is so that we can check whether this has failed or not. If this fails, we throw an error. The error we throw is the data decoded as an ErrorResponse, or whatever error is thrown during the decoding of that.
In your view model, you can check the error like this:
.sink { completion in
switch completion {
case .failure(let error as ErrorResponse):
// wrong password/username
// you can access error.message here
case .failure(let error):
// some other sort of error:
default:
break
}
} receiveValue: { loginResponse in
...
}
You can make use of tryMap with combine to figure out where the function should return. I'd suggest you take a read of the documentation on it but here is a snippet that should be able to get you moving with it.
Hopefully this is what you mean by the question - I've changed a few things but feel free to take the code as a building block and adapt as needed!
enum LoginError: Error, Equatable {
case noConnection
case invalidResponse
}
static func login(email: String, password: String) -> AnyPublisher<Void, LoginError> {
return URLSession.shared
.dataTaskPublisher(for: request)
.print()
.receive(on: DispatchQueue.main)
.mapError { _ in LoginError.noConnection }
.tryMap { (data, response) in
guard let response = response as? HTTPURLResponse else {
throw LoginError.invalidResponse
}
if response.statusCode == 200 {
return data
} else {
throw LoginError.invalidResponse
}
}
.decode(type: LoginResponse.self, decoder: JSONDecoder())
.tryMap { [unowned self] in
// Update your session with the token here using $0 then return.
// e.g session.token = $0.token
return
}
.mapError { $0 as? LoginError ?? .invalidResponse }
.eraseToAnyPublisher()
}

Alamofire response is nil

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.

MVC Networking Swift

I have this Networking class that i declared in the Model .
class Networking {
func response (url : String ) {
guard let url = URL(string: url) else {return}
URLSession.shared.dataTask(with: url, completionHandler: urlPathCompletionHandler(data:response:error:)).resume()
}
func urlPathCompletionHandler (data : Data? , response: URLResponse? , error: Error? ) {
guard let data = data else {return }
do {
let jsondecoder = JSONDecoder()
}catch {
print("Error \(error)")
}
}
}
In the controller . I have an array of users i declared and i want the controller to call from the Model Networking class instead of doing the networking inside the controller. This is part of my controller:
var users = [Users]()
var networking : Networking()
#IBOutlet weak var tableview : UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableview.delegate = self
tableview.dataSource = self
}
func getFromModel() {
var vm = networking.response()
}
I want a way of calling the networking class and return an array of users that i can set to the users array above and use it to populate the table view . If i wanted to do that inside the controller it would easy but i am not sure how i can return an array of users from the Model Networking class .
You need to modify your Network class like this:
class Networking {
func response<T: Codable>(url: String, completion: ((T) -> Void)?) {
guard let url = URL(string: url) else {return}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
self.urlPathCompletionHandler(data: data, response: response, error: error, completion: completion)
}).resume()
}
func urlPathCompletionHandler<T: Codable>(data : Data? , response: URLResponse? , error: Error?, completion: ((T) -> Void)?) {
guard let data = data else { return }
do {
let jsondecoder = JSONDecoder()
// Pseudo Code to decode users
completion?(decodedObject)
} catch {
print("Error \(error)")
}
}
}
And call it like this:
func getFromModel() {
networking.response(url: <#T##String#>) { (users: [User]) in
self.users = users
}
}
OK, there are a few thoughts:
Your response method is performing an asynchronous network request, so you need to give it a completion handler parameter. So, I might suggest something like:
class Networking {
enum NetworkingError: Error {
case invalidURL
case failed(Data?, URLResponse?)
}
private let parsingQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".parsing")
// response method to handle network stuff
func responseData(_ string: String, completion: #escaping (Result<Data, Error>) -> Void) {
guard let url = URL(string: string) else {
completion(.failure(NetworkingError.invalidURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
completion(.failure(NetworkingError.failed(data, response)))
return
}
completion(.success(responseData))
}
}.resume()
}
// response method to handle the JSON parsing
func response<T: Decodable>(of type: T.Type, from string: String, completion: #escaping (Result<T, Error>) -> Void) {
responseData(string) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
self.parsingQueue.async {
do {
let responseObject = try JSONDecoder().decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(responseObject))
}
} catch let parseError {
DispatchQueue.main.async {
completion(.failure(parseError))
}
}
}
}
}
}
}
This obviously assumes that you have some Codable types. For example, it’s common for an API to have some common structure in its responses:
struct ResponseObject<T: Decodable>: Decodable {
let code: Int
let message: String?
let data: T?
}
And maybe the User is like so:
struct User: Decodable {
let id: String
let name: String
}
Then getFromModel (perhaps better called getFromRepository or something like that) could parse it with:
networking.response(of: ResponseObject<[User]>.self, from: urlString) { result in
switch result {
case .failure(let error):
print(error)
case .success(let responseObject):
let users = responseObject.data
// do something with users
}
}
For what it’s worth, if you didn’t want to write your own networking code, you could use Alamofire, and then getFromModel would do:
AF.request(urlString).responseDecodable(of: ResponseObject<[User]>.self) { response in
switch response.result {
case .failure(let error):
print(error)
case .success(let responseObject):
let users = responseObject.data
}
}
Now, clearly the model types are likely to be different in your example, but you didn’t share what your JSON looked like, so I had to guess, but hopefully the above illustrates the general idea. Make a generic-based network API and give it a completion handler for its asynchronous responses.

How can I use Alamofire Router to organize the API call? [swift/ Alamofire5]

I'm trying to convert my AF request to Router structures for a cleaner project. I'm getting an error for:
Value of protocol type 'Any' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols.
Please help me to fix the code. THANK YOU!
My URL will have a placeholder for username and the password will be sent in body. The response will be Bool (success), username and bearer token.
Under is my AF request:
let username = usernameTextField.text
let password = passwordTextField.text
let loginParams = ["password":"\(password)"]
AF.request("https://example.com/users/\(username)/login",
method: .post,
parameters: loginParams,
encoder: JSONParameterEncoder.default,
headers: nil, interceptor: nil).response { response in
switch response.result {
case .success:
if let data = response.data {
do {
let userLogin = try JSONDecoder().decode(UsersLogin.self, from: data)
if userLogin.success == true {
defaults.set(username, forKey: "username")
defaults.set(password, forKey: "password")
defaults.set(userLogin.token, forKey: "token")
print("Successfully get token.")
} else {
//show alert
print("Failed to get token with incorrect login info.")
}
} catch {
print("Error: \(error)")
}
}
case .failure(let error):
//show alert
print("Failed to get token.")
print(error.errorDescription as Any)
}
}
What I have so far for converting to AF Router structures:
import Foundation
import Alamofire
enum Router: URLRequestConvertible {
case login(username: String, password: String)
var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
var path: String {
switch self {
case .login(let username):
return "/users/\(username)/login"
}
}
var parameters: Parameters? {
switch self {
case .login(let password):
return ["password": password]
}
}
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try Constants.ProductionServer.baseURL.asURL()
var request = URLRequest(url: url.appendingPathComponent(path))
// HTTP Method
request.httpMethod = method.rawValue
// Common Headers
request.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue)
request.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
// Parameters
switch self {
case .login(let password):
request = try JSONParameterEncoder().encode(parameters, into: request) //where I got the error
}
return request
}
}
class APIClient {
static func login(password: String, username: String, completion: #escaping (Result<UsersLogin, AFError>) -> Void) {
AF.request(Router.login(username: username, password: password)).responseDecodable { (response: DataResponse<UsersLogin, AFError>) in
completion(response.result)
}
}
}
LoginViewController Class (where I replaced the AF.request code)
APIClient.login(password: password, username: username) { result in
switch result {
case .success(let user):
print(user)
case .failure(let error):
print(error.localizedDescription)
}
Codable UsersLogin model
struct UsersLogin: Codable {
let success: Bool
let username: String
let token: String?
enum CodingKeys: String, CodingKey {
case success = "success"
case username = "username"
case token = "token"
}
}
Took me a while but finally fixed it. I also clean up the code too.
enum Router: URLRequestConvertible {
case login([String: String], String)
var baseURL: URL {
return URL(string: "https://example.com")!
}
var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
var path: String {
switch self {
case .login(_, let username):
return "/users/\(username)/login"
}
}
func asURLRequest() throws -> URLRequest {
print(path)
let urlString = baseURL.appendingPathComponent(path).absoluteString.removingPercentEncoding!
let url = URL(string: urlString)
var request = URLRequest(url: url!)
request.method = method
switch self {
case let .login(parameters, _):
request = try JSONParameterEncoder().encode(parameters, into: request)
}
return request
}
}
Usage
let username = usernameTextField.text
AF.request(Router.login(["password": password], username)).responseDecodable(of: UsersLogin.self) { (response) in
if let userLogin = response.value {
switch userLogin.success {
case true:
print("Successfully get token.")
case false:
print("Failed to get token with incorrect login info.")
}
} else {
print("Failed to get token.")
}
}
I solved a similar problem in this way. I created a protocol Routable
enum EncodeMode {
case encoding(parameterEncoding: ParameterEncoding, parameters: Parameters?)
case encoder(parameterEncoder: ParameterEncoder, parameter: Encodable)
}
protocol Routeable: URLRequestConvertible {
var baseURL: URL { get }
var path: String { get }
var method: HTTPMethod { get }
var encodeMode: EncodeMode { get }
}
extension Routeable {
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = baseURL.appendingPathComponent(path)
var urlRequest: URLRequest
switch encodeMode {
case .encoding(let parameterEncoding, let parameters):
urlRequest = try parameterEncoding.encode(URLRequest(url: url), with: parameters)
case .encoder(let parameterEncoder, let parameter):
urlRequest = URLRequest(url: url)
urlRequest = try parameterEncoder.encode(AnyEncodable(parameter), into: urlRequest)
}
urlRequest.method = method
return urlRequest
}
}
And my routers look like this one
enum WifiInterfacesRouter: Routeable {
case listActive(installationId: Int16?)
case insert(interface: WifiInterface)
var encodeMode: EncodeMode {
switch self {
case .listActive(let installationId):
guard let installationId = installationId else {
return .encoding(parameterEncoding: URLEncoding.default, parameters: nil)
}
return .encoding(parameterEncoding: URLEncoding.default, parameters: ["idInstallation": installationId])
case .insert(let interface):
return .encoder(parameterEncoder: JSONParameterEncoder.default, parameter: interface)
}
}
var baseURL: URL {
return URL(string: "www.example.com/wifiInterfaces")!
}
var method: HTTPMethod {
switch self {
case .listActive: return .get
case .insert: return .post
}
}
var path: String {
switch self {
case .listActive: return "listActive"
case .insert: return "manage"
}
}
}
To solve the build error
Protocol 'Encodable' as a type cannot conform to the protocol itself
I used the useful AnyCodable library. A type erased implementation of Codable.
You can't use Parameters dictionaries with Encodable types, as a dictionary of [String: Encodable] is not Encodable, like the error says. I suggest moving that step of the asURLRequest process into a separate function, such as:
func encodeParameters(into request: inout URLRequest) {
switch self {
case let .login(parameters):
request = try JSONParameterEncoder().encode(parameters, into: request)
}
}
Unfortunately this doesn't scale that well for routers with many routes, so I usually break up my routes into small enums and move my parameters into separate types which are combined with the router to produce the URLRequest.

Using Generics / Codable w/ API response 204 NO CONTENT

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!)
}