【问题标题】:Handle backend validation errors with URLSession concurrency使用 URLSession 并发处理后端验证错误
【发布时间】:2022-01-21 03:42:27
【问题描述】:

是否可以解码来自服务器的错误响应,该响应在对象内部具有未知键?我将如何处理这样的回应?

现在我像这样在 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."
      ]
    }
  }
}

数据和错误键始终存在,但根据我所做的请求和后端验证,“登录”可能会有所不同。所以“错误”也可以有多个键。

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

【问题讨论】:

  • 您可以创建一个可编码模型,其中errors[String: [String]],或者使用自定义初始化,使用枚举关联值来处理login 键和其他(如果你知道什么更好可以是所有不同的键)...
  • FWIW,你没有配置你的JSONEncoder。你可能也想做日期编码/解码策略之类的事情。就个人而言,我建议将其移动到 API 服务对象中,而不是 URLSession 扩展中,避免重复重新配置这些帮助对象。此外,您的身份验证方案对于您的 API 是唯一的,而不是 URLSession。我理解URLSession扩展的吸引力,但它确实属于API层,而不是网络层。

标签: swift concurrency urlsession


【解决方案1】:

您可以为 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.)
}

【讨论】:

  • 非常感谢!这就是我一直在寻找的。我只是不明白如何读取登录功能的 catch 块中的错误。另外关于您的其他评论,我完全同意您的观点,我仍然需要进行很多重构。我是 swift/swiftUI 的新手,从 hackingwithswift 看到了这个例子。我将尝试按照您的建议创建一个 Api 对象。
  • 没关系,我现在可以读取错误了} ```
猜你喜欢
  • 2012-08-17
  • 2011-10-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-11
  • 2017-08-19
  • 2017-07-04
  • 1970-01-01
相关资源
最近更新 更多