如何在 SwiftUI URLSession 中显示来自服务器的错误消息

How to Display Error Message from Server in SwiftUI URLSession

我正在构建一个与 API 交互的 SwiftUI 应用程序。用户通过点击我的 Rails 服务器的注册表单创建一个帐户。一切正常,但我不知道如何显示来自服务器的错误消息。

例如,注册表单中的电子邮件地址和用户名必须是唯一的,并且与现有用户的不匹配。这可能是一个罕见的问题,但服务器实际上 returns 一条 422 消息表明用户创建失败,因为用户名或电子邮件或两者已经存在。

如何显示这些特定错误而不是典型的通用 URLSession 枚举错误?

另外,有没有办法在注册功能 运行 之前检查用户名和电子邮件地址是否已经存在,因为这是设置它的理想方式。它在网络版本上以这种方式工作。

这是尝试失败后来自服务器的错误 JSON:

{
"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"
    ]
}
}

这是我发出注册请求的注册服务:

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

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

在没有看到 LoginResponse 结构的情况下,我的猜测是:

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

失败了。

我看到两种处理方式:

  • 更新 LoginResponse 也能够处理错误消息的结构(使用可选属性等)
  • 创建一个单独的 ErrorResponse 结构来处理错误消息的结构,并在解码到 LoginResponse 失败时尝试解码到该结构

我的意见是第二个更清晰,因为如果返回错误,您会立即知道,而不是在解码后必须检查 loginResponse 变量的结构。

ErrorResponse 结构可能类似于:

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

并且data的解码可以修改为:

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
}

这样做的一个额外好处是,您可以更改 LoginResponse 以拥有更少的可选属性,然后鉴于这种更强的类型,您可以删除函数中的大部分后续保护子句。

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.

如果 Rails 应用程序使用 API 端点执行 username 等的预验证,那么当然,您应该能够使用相同的端点提前检查。只需在打开开发人员工具的情况下访问该站点,然后查看它发出的请求。

我最终接受了上面的答案并将其合并到我的服务电话中。我需要添加结构来匹配 JSON 错误代码。然后我需要将这些结构构建到错误响应中以展开可选项。

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
}


// MARK: - BadSignUpResponse
struct BadSignUpResponse: Error, Codable {
let status: String
let data: DataClass
let errors: Errors
}

struct DataClass: Codable {
let id: Int?
let email: String?
let createdAt, updatedAt: String?
let firstName, lastName, streetAddress1, streetAddress2: String?
let city, state: String?
let country: String?
let username, companyName: String?
let zipCode, phoneNumber, stripeCustID: String?
let availableCredits: Int?
let provider, uid: String?
let allowPasswordChange, removeMyAccount: Bool?
}

// MARK: - Errors
struct Errors: Codable {
let email: [String]?
let username: [String]?
let full_messages: [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, BadSignUpResponse>) -> Void) {
        
    guard let url = URL(string: "https://example.com/api/v1/auth") else {
        completed(.failure(BadSignUpResponse.init(status: "url error",
                                                  data: DataClass.init(id: 0, email: "", createdAt: "", updatedAt: "",
                                                                       firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", state: "", country: "", username: "", companyName: "", zipCode: "", phoneNumber: "", stripeCustID: "", availableCredits: 0, provider: "", uid: "", allowPasswordChange: false, removeMyAccount: false),

                                                  errors: Errors.init(email: [], username: [],
                                                                      full_messages: []))))
        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 data = data, error == nil else { return }
            
            guard let token = response.value(forHTTPHeaderField: "access-token")  else {
                guard let errorResponse = try? JSONDecoder().decode(BadSignUpResponse.self, from: data) else {
                    completed(.failure(BadSignUpResponse.init(status: "error",
                                                              data: DataClass.init(id: 0, email: "", createdAt: "", updatedAt: "",
                                                                                   firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", state: "", country: "", username: "", companyName: "", zipCode: "", phoneNumber: "", stripeCustID: "", availableCredits: 0, provider: "", uid: "", allowPasswordChange: false, removeMyAccount: false),
                                                              errors: Errors.init(email: [], username: [], full_messages: []))))
                return
            }
                completed(.failure(errorResponse))
            return
            }
        
            
            
            guard let client = response.value(forHTTPHeaderField: "client") else {
                guard let errorResponse = try? JSONDecoder().decode(BadSignUpResponse.self, from: data) else {
                    completed(.failure(BadSignUpResponse.init(status: "error",
                                                              data: DataClass.init(id: 0, email: "", createdAt: "", updatedAt: "",
                                                                                   firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", state: "", country: "", username: "", companyName: "", zipCode: "", phoneNumber: "", stripeCustID: "", availableCredits: 0, provider: "", uid: "", allowPasswordChange: false, removeMyAccount: false),
                                                              errors: Errors.init(email: [], username: [], full_messages: []))))
                return
            }
                completed(.failure(errorResponse))
            return
            }
      
            guard let loginResponse = try? JSONDecoder().decode(LoginResponse.self, from: data) else {
                guard let errorResponse = try? JSONDecoder().decode(BadSignUpResponse.self, from: data) else {
                    completed(.failure(BadSignUpResponse.init(status: "login response error",
                                                              data: DataClass.init(id: 0, email: "", createdAt: "", updatedAt: "",
                                                                                   firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", state: "", country: "", username: "", companyName: "", zipCode: "", phoneNumber: "", stripeCustID: "", availableCredits: 0, provider: "", uid: "", allowPasswordChange: false, removeMyAccount: false),
                                                              errors: Errors.init(email: [], username: [], full_messages: []))))
                return
            }
                completed(.failure(errorResponse))
            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()
}
}