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

soundflix
  • 928
  • 9
  • 22
soleil
  • 12,133
  • 33
  • 112
  • 183
  • *"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."* – Leo Dabus Jul 13 '23 at 02:43
  • How would you expect to decode the data if the error is not nil ? – Leo Dabus Jul 13 '23 at 02:44

1 Answers1

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...
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • 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. – soleil Jul 13 '23 at 12:26
  • The second suggestion doesn't compile: `Cannot convert value of type 'Request' to expected argument type 'URLRequest'`. Are you sure that `request` should be a `URLRequest` in the `decode` func? – soleil Jul 13 '23 at 12:31
  • @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. – Sweeper Jul 13 '23 at 12:33
  • @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 Jul 13 '23 at 12:48
  • @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. – JeremyP Jul 13 '23 at 13:01
  • @JeremyP yeah, which is why I suspected that the server gave an unexpected response, causing the decoding to fail. – Sweeper Jul 13 '23 at 13:06
  • It's actually a 404 error, sorry. But I have it working now with the updated `Request` code. – soleil Jul 13 '23 at 13:22