Search code examples
jsonswiftswiftuihttpresponseurlsession

How to Display Error Message from Server in SwiftUI URLSession


I'm building a SwiftUI app that interfaces with an API. A user creates an account through a signup form that hits my Rails server. Got that working fine, but I have no idea how to display the error message from the server.

For example, the email address and username in the signup form need to be unique and not match those of an existing user. It would likely be a rare problem, but the server actually returns a 422 message that the user creation failed because the username or email or both already exist.

How do I display these specific errors and not the typical generic URLSession enum error?

Also, is there a way to check if the username and email address already exist BEFORE the signup function is run because that would be the ideal way to set it up. It works that way on the web version.

Here is the error JSON from the server after a failed attempt:

{
"status": "error",
"data": {
    "id": null,
    "email": "sampleuser3@example.com",
    "created_at": null,
    "updated_at": null,
    "username": "SampleUser3"

},
"errors": {
    "email": [
        "has already been taken"
    ],
    "username": [
        "has already been taken"
    ],
    "full_messages": [
        "Email has already been taken",
        "Username has already been taken"
    ]
}
}

Here is my signup service that makes the signup request:

import Foundation

struct SignUpRequestBody: Codable {
let email: String
let username: String
let firstName: String
let lastName: String
let phoneNumber: String
let password: String
let passwordConfirmation: String
let category: String
}


final class SignUpService {

static let shared = SignUpService()

func signUp(email: String, username: String, firstName: String, lastName: String, phoneNumber: String,  password: String, passwordConfirmation: String, category: String, completed: @escaping (Result<SessionToken, AuthenticationError>) -> Void) {
        
    guard let url = URL(string: "https://example.com/auth") else {
        completed(.failure(.custom(errorMessage:"URL unavailable")))
        return
    }

    let body = SignUpRequestBody(email: email.lowercased(), username: username, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, password: password, passwordConfirmation: passwordConfirmation, category: "consumer")
    
    var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try? JSONEncoder().encode(body)
    
    URLSession.shared.dataTask(with: request) { (data, response, error) in
        
        if let response = response as? HTTPURLResponse {

            guard let token = response.value(forHTTPHeaderField: "access-token") else {
                completed(.failure(.custom(errorMessage: "Missing Access Token")))
                return
            }
            guard let client = response.value(forHTTPHeaderField: "client") else {
                completed(.failure(.custom(errorMessage: "Missing Client")))
                return
            }
      
            guard let data = data, error == nil else { return }
            guard let loginResponse = try? JSONDecoder().decode(LoginResponse.self, from: data) else { return }

            guard let messageData = loginResponse.data else {return}
            guard let message = messageData.message else {return}
    
            guard let userRole = messageData.user else {return}
            guard let role = userRole.role else {return}
        
            let sessionToken = SessionToken(accessToken: token, client: client, message: message, role: role)
            
            completed(.success(sessionToken))
        }
    }.resume()
}
}

Solution

  • How do I display these specific errors and not the typical generic URLSession enum error?

    My guess, without seeing the LoginResponse struct, is that:

    try? JSONDecoder().decode(LoginResponse.self, from: data)
    

    is failing.

    I see two options for how to deal with this:

    • update LoginResponse to be also able to handle the structure of the error message (using optional properties etc)
    • create a separate ErrorResponse struct to handle the structure of the error message and attempt decoding to that struct if decoding to LoginResponse fails

    My opinion is that the second is cleaner, as you will know immediately if there is an error returned, as opposed to having to check the structure of the loginResponse variable after decoding.

    The ErrorResponse struct could look something like:

    struct ErrorResponse: Codable {
      let status: String
      let data: Dictionary<String, String?>
      let errors: Dictionary<String, [String]>
    }
    

    and the decoding of data could be modified to:

    guard let loginResponse = try? JSONDecoder().decode(LoginResponse.self, from: data) else {
      guard let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) else {
        completed(.failure(.custom(errorMessage: "Unable to decode as either `LoginResponse` or `ErrorResponse`")))
        return
      }
    
      completed(.failure(.custom(errorMessage: errorResponse.errors.description)))
      return
    }
    

    An added bonus of this is that you can change LoginResponse to have fewer optional properties, and then given this stronger typing, you can remove much of the subsequent guard clauses in the function.

    Also, is there a way to check if the username and email address already exist BEFORE the signup function is run because that would be the ideal way to set it up. It works that way on the web version.

    If the Rails app performs the pre-validation of username etc using an API endpoint, then sure, you should be able to check in advance using the same endpoint. Just visit the site with developer tools open and see what requests it makes.