Home > other >  How to decode error response message in Combine?
How to decode error response message in Combine?

Time:01-03

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

CodePudding user response:

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

CodePudding user response:

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()
}
  • Related