I got the following code online and I am trying to modify it in a way that I can get access to the decoded response for 401. Server sends the following 401 response as JSON.
{ message: 'Username or password is incorrect', success: false }
But when I use the HTTPClient below it simply throws NetworkError.unauthorized without giving me the message. How can I update the load function to get access of the server response.
struct HTTPClient {
private let session: URLSession
init() {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = ["Content-Type": "application/json"]
self.session = URLSession(configuration: configuration)
}
func load<T: Codable>(_ resource: Resource<T>) async throws -> T {
var request = URLRequest(url: resource.url)
// Set HTTP method and body if needed
switch resource.method {
case .get(let queryItems):
var components = URLComponents(url: resource.url, resolvingAgainstBaseURL: false)
components?.queryItems = queryItems
guard let url = components?.url else {
throw NetworkError.badRequest
}
request.url = url
case .post(let data), .put(let data):
request.httpMethod = resource.method.name
request.httpBody = data
case .delete:
request.httpMethod = resource.method.name
}
// Set custom headers
if let headers = resource.headers {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
// Check for specific HTTP errors
switch httpResponse.statusCode {
case 200...299:
break // Success
case 400:
throw NetworkError.badRequest
case 401:
throw NetworkError.unauthorized
case 403:
throw NetworkError.forbidden
case 404:
throw NetworkError.notFound
default:
throw NetworkError.unknownStatusCode(httpResponse.statusCode)
}
do {
let result = try JSONDecoder().decode(resource.modelType, from: data)
return result
} catch {
throw NetworkError.decodingError(error)
}
}
}
UPDATE:
NetworkError Looks like the following:
extension NetworkError: LocalizedError {
var errorDescription: String? {
switch self {
case .badRequest:
return NSLocalizedString("Bad Request (400): Unable to perform the request.", comment: "badRequestError")
case .unauthorized:
return NSLocalizedString("Unauthorized (401): Authentication is required.", comment: "unauthorizedError")
case .forbidden:
return NSLocalizedString("Forbidden (403): You don't have permission to access this resource.", comment: "forbiddenError")
case .notFound:
return NSLocalizedString("Not Found (404): The requested resource could not be found.", comment: "notFoundError")
case .serverError(let errorMessage):
return NSLocalizedString(errorMessage, comment: "serverError")
case .decodingError:
return NSLocalizedString("Unable to decode successfully.", comment: "decodingError")
case .invalidResponse:
return NSLocalizedString("Invalid response.", comment: "invalidResponse")
case .unknownStatusCode(let statusCode):
return NSLocalizedString("Unknown error with status code: \(statusCode).", comment: "unknownStatusCodeError")
}
}
}
UPDATE 2:
I updated the load function and now when I get 401 I throw LoginError. Does that look right? Since HTTPClient is a generic network layer it feels weird to throw such specific errors?
// Check for specific HTTP errors
switch httpResponse.statusCode {
case 200...299:
break // Success
case 400:
throw NetworkError.badRequest
case 401:
let response = try? JSONDecoder().decode(ErrorResponse.self, from: data)
throw LoginError.loginFailed(response?.message ?? "Login error")
case 403:
throw NetworkError.forbidden
case 404:
throw NetworkError.notFound
default:
throw NetworkError.unknownStatusCode(httpResponse.statusCode)
}
UPDATE 3:
Inside the HTTPClient load function, I updated the code to throw the same NetworkError as shown below:
case 401:
let response = try JSONDecoder().decode(ErrorResponse.self, from: data)
throw NetworkError.unauthorized(response)
The caller (AuthenticationController) handles the call and then throw the appropriate error.
func login(username: String, password: String) async throws -> LoginResponse {
let body = ["username": username, "password": password]
let bodyData = try JSONEncoder().encode(body)
let resource = Resource(url: Constants.login, method: .post(bodyData), modelType: LoginResponse.self)
do {
let response = try await httpClient.load(resource)
return response
} catch NetworkError.unauthorized(let errorResponse) {
throw LoginError.loginFailed(errorResponse.message ?? "Unable to login.")
} catch {
throw error
}
}
I would just simplify NetworkError
to:
enum NetworkError: Error {
case badRequest
case invalidResponse
case decodingError(Error)
case errorResponse(response: HTTPURLResponse, body: Data)
}
The first three cases are thrown from load
before you receive the HTTPURLResponse
. After you got the response, just throw the errorResponse
case and let the caller deal with it in their own way.
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
break
default:
throw NetworkError.errorResponse(response: httpResponse, body: data)
}
Alternatively, if a Resource<T>
knows the type of error it will get, you can add an additional type parameter:
struct Resource<T, Failure> where T: Codable, Failure: Error, Failure: Codable {
// ...
}
Then, you can write load
as:
func load<T, Failure>(_ resource: Resource<T, Failure>) async throws -> T
where T: Codable, Failure: Codable, Failure: Error
{
// ...
switch httpResponse.statusCode {
case 200...299:
break
default:
let error = try JSONDecoder().decode(Failure.self, from: data)
throw error
}
}
You can also decode the error conditionally - check if Failure.self == Never.self
. If that is true, throw a NetworkError.errorResponse(...)
as before. This way, you can have Resource
s that don't know what their error response will be, written as Resource<SomeModel, Never>
.
Example usage:
struct Person: Codable {
struct ErrorResponse: Error, Codable {
let message: String
let success: Bool
}
let name: String
let age: Int
}
func example() async throws {
let resource: Resource<Person, Person.ErrorResponse> = ...
do {
print(try await load(resource))
} catch let error as NetworkError {
// ...
} catch let error as Person.ErrorResponse {
// ...
}
}