使用 URLSession 并发处理后端验证错误

Handle backend validation errors with URLSession concurrency

是否可以解码来自对象内部密钥未知的服务器的错误响应?我将如何处理这样的回复?

现在我像这样对 URLSession 进行了扩展

extension URLSession {
    func post<T: Decodable, U: Encodable>(
        _ type: T.Type = T.self,
        data: U,
        from url: URL,
        keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .convertFromSnakeCase
    ) async throws  -> T {
        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"
        
        let body = try JSONEncoder().encode(data)
        
        let token = UserDefaults.standard.string(forKey: "AccessToken")
        if token != nil {
            request.setValue("Bearer \(token!)", forHTTPHeaderField: "Authorization")
        }
        do {
            let (data, response) = try await upload(for: request, from: body)
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = keyDecodingStrategy

            let decoded = try decoder.decode(T.self, from: data)
            return decoded
        } catch {
            print(error)
            throw error
        }
        
    }
}

这样我就可以向后端发出 POST 请求并取回解码后的对象 所以当我尝试登录时,我可以这样做:

    func login(email: String, password: String) async {
        let body = LoginRequest(email: email, password: password)
        let url = URL(string: "https://api.junoreader.com/api/auth/login")!
        do {
            let response = try await URLSession.shared.post(DetailModel<UserModel>.self, data: body, from: url)
            setUser(data: response.data)
        } catch {
            print("api error")
            print(error)
        }
    }

响应的样子

struct DetailModel<T: Codable>: Codable {
    var data: T
}
struct UserModel: Codable, Identifiable, ObservableObject {
    let id: Int
    let firstName: String
    let lastName: String
    let username: String
    let bio: String

    var name: String {
        return "\(firstName ?? "") \(lastName ?? "")"
    }
}

但是当登录凭据错误时,服务器会以 403 响应 JSON 对象,如下所示

{
  "data": {
    "errors": {
      "login": [
        "Email/password do not match."
      ]
    }
  }
}

数据和错误键始终存在,但 'login' 可能会根据我的请求和后端验证而有所不同。所以 'error' 也可以有多个键。

解码此类错误对象的最佳方法是什么?如何在 'post' 函数中处理这些错误?同样,当错误发生而不是立即抛出错误时,swift 仍在尝试解码数据。

您可以为 API 个错误定义一个对象:

struct ApiErrorPayload: Decodable {
    let errors: [String: [String]]
}

然后您可以定义一个 Error 枚举,以便抛出此错误(和一般 HTTP 错误):

enum ApiError: Error {
    case serviceError(ApiErrorPayload)
    case httpError(Data, HTTPURLResponse)
}

然后您可以定义 post 方法来检查状态代码并解码您的错误对象或 ApiErrors:

extension URLSession {
    func post<T: Decodable, U: Encodable>(
        _ type: T.Type = T.self,
        data: U,
        from url: URL,
        keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .convertFromSnakeCase
    ) async throws  -> T {
        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"

        let body = try JSONEncoder().encode(data)

        if let token = UserDefaults.standard.string(forKey: "AccessToken") {
            request.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
        }

        let (data, response) = try await upload(for: request, from: body)
        guard let response = response as? HTTPURLResponse else {
            throw URLError(.badServerResponse)
        }

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = keyDecodingStrategy

        switch response.statusCode {
        case 200 ..< 300:
            return try decoder.decode(T.self, from: data)

        case 400 ..< 500:
            let payload = try decoder.decode(ApiErrorPayload.self, from: data)
            throw ApiError.serviceError(payload)

        default:
            throw ApiError.httpError(data, response)
        }
    }
}

如果你想捕获错误,你可以这样做:

do {
    let user = try await session.post(DetailModel<UserModel>.self, data: foo, from: url)
    // do something with `user`
} catch let ApiError.serviceError(payload) {
    // present API errors in the UI
} catch let ApiError.httpError(data, response) {
    // handle general web service errors here (e.g. perhaps a nice user-friendly message if response.statusCode == 500 and log the error in Crashlytics or some error handling system)
} catch URLError.notConnectedToInternet {
    // let user know they're not connected to internet
} catch {
    // failsafe for other errors (e.g. parsing problems, etc.)
}