0

Say I'm making an API call like this:

Task {
    do {
        let request: Request<LoginResponse> = .postLogin(email: email, password: password)
        let response = try await URLSession.shared.decode(request)
        // store token if received
    } catch {
        // ???
    }
}

struct LoginResponse: Decodable {
    let token: String
    let msg: String
}

extension URLSession {
    func decode<Value: Decodable>(
        _ request: Request<Value>,
        using decoder: JSONDecoder = .init()
    ) async throws -> Value {
        let decoded = Task.detached(priority: .userInitiated) {
            let (data, _) = try await self.data(for: request.urlRequest)
            try Task.checkCancellation()
            return try decoder.decode(Value.self, from: data)
        }
        return try await decoded.value
    }
}

In the catch block of the Task, how would I access the "msg" from LoginResponse? It's an error message being returned from the backend that the login info is incorrect. Or how would I restructure the do/catch so that I can access the LoginResponse object in the catch block?

For further info on the Request object, see here

2
  • "If the request completes successfully, the data parameter of the completion handler block contains the resource data, and the error parameter is nil. If the request fails, the data parameter is nil and the error parameter contain information about the failure." Commented Jul 13, 2023 at 2:43
  • How would you expect to decode the data if the error is not nil ? Commented Jul 13, 2023 at 2:44

1 Answer 1

1

If your backend still gives you an HTTP response if the login failed, then try await self.data(for: request.urlRequest) won't throw an error. It will return a HTTPURLResponse (which you completely ignored with _) with a statusCode indicating an error.

self.data(for: request.urlRequest) would only throw when there is no response at all, like when you used an invalid URL, or there is no internet connection.

Therefore, you can return the HTTPURLResponse back to the caller, and check it in the do block:

func decode<Value: Decodable>(
    _ request: Request<Value>,
    using decoder: JSONDecoder = .init()
) async throws -> (Value, HTTPUURLResponse?) {
    let decoded = Task.detached(priority: .userInitiated) {
        let (data, response) = try await self.data(for: request.urlRequest)
        try Task.checkCancellation()
        return try (
            decoder.decode(Value.self, from: data), 
            response as? HTTPURLResponse // this cast will succeed if the request is an HTTP request
        )
    }
    return try await decoded.value
}
let request: Request<LoginResponse> = .postLogin(email: email, password: password)
let (decodedData, response) = try await URLSession.shared.decode(request)
if let httpResponse = response, httpResponse.statusCode >= 400 {
    // handle error with decodedData...
}

If you want to handle it in the catch block instead, you can check this in the task, and throw an error instead.

func decode<Value: Decodable, Failure: Error & Decodable>(
    _ request: Request<Value>,
    using decoder: JSONDecoder = .init(),
    errorResponseType: Failure.Type
) async throws -> Value {
    let decoded = Task.detached(priority: .userInitiated) {
        let (data, response) = try await self.data(for: request.urlRequest)
        try Task.checkCancellation()
        if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 {
            throw try decoder.decode(errorResponseType, from: data)
        }
        return try decoder.decode(Value.self, from: data)
    }
    return try await decoded.value
}
// these could be the same type, but I prefer separating errors from successful responses
struct LoginResponse: Decodable {
    let token: String
    let msg: String
}

struct LoginError: Decodable, Error {
    let token: String
    let msg: String
}
do {
    let request: Request<LoginResponse> = .postLogin(email: email, password: password)
    let decodedData = try await URLSession.shared.decode(request, errorResponseType: LoginError.self)
} catch let error as LoginError {
    // handle login error...
} catch {
    // handle other errors...
}
Sign up to request clarification or add additional context in comments.

7 Comments

Thank you for this. I tried your first suggestion, but the if let httpResponse = response... block does not get called, even though backend is returning a 422 error.
The second suggestion doesn't compile: Cannot convert value of type 'Request<LoginResponse>' to expected argument type 'URLRequest'. Are you sure that request should be a URLRequest in the decode func?
@soleil Sorry, I used URLRequest in my own code instead of copy-pasting your Request type. Anyway, if the if let didn't run, did it go into the catch block instead then? What is the error that is caught? Or did one of the checks fail? Help me help you here and do some debugging yourself.
@soleil It is also strange that you got a 422. That doesn't look like a "login failed". I would expect a 401 or 403. Did the decoding fail? Note that I have assumed the decoding of Value succeeds if the status code is 2xx.
@Sweeper 422 is "cannot process content". It means that the JSON body in this case contains correct JSON but the server doesn't know how to process it. Essentially, it is not expecting a request containing whatever .postLogin(...) creates.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.