使用 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.)
}
是否可以解码来自对象内部密钥未知的服务器的错误响应?我将如何处理这样的回复?
现在我像这样对 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.)
}