Swift URLSession 如何统一处理带 token 和不带 token 的请求?
可以共用一个请求方法。底层请求方法可以将 token 设为可选参数:有非空值时添加 Authorization 请求头,否则省略。是否必须登录,由接口的认证要求决定。
原代码需要调整返回结果和错误处理:用 Result<Data, Error> 替代 (Data?, Any?),检查 HTTP 状态码,并确保无效 URL 等失败路径也会调用 completion handler。
原代码有哪些问题?
方法名与实际请求不一致
newPostRequest 设置的是:
request.httpMethod = "GET"
如果目标是读取数据,可以将方法改名为 get。如果接口要求 POST,则需要按照接口约定修改 HTTP 方法和请求体。由于当前问题没有提供 POST 请求体,下面的示例保留 GET。
没有 token 不一定是错误
当前代码在 token 为空时打印 invalid Token,随后仍然发送请求。公开接口不传 token 是正常情况,不需要打印错误。
可以将参数改成:
token: String? = nil
这样调用公开接口时就可以省略 token。本地检查只能判断 token 是否存在,不能判断它是否有效、过期或具备权限;认证结果仍由服务端决定。
completion handler 存在含义不清的状态
原来的类型是:
typealias completion = ((Data?, Any?) -> ())
Any? 无法明确表达错误类型,completionHandler(nil, nil) 也让调用方无法判断失败原因。此外,URL 无效时直接 return,调用方收不到完成通知,加载状态可能一直无法结束。
改成 Result<Data, Error> 后,结果分为成功和失败两种情况。
不能只检查 error
URLSession 回调中的 error 主要用于报告传输等错误。收到 HTTP 401、404 或 500 时,error 仍可能为 nil,而 data 中可能是服务端返回的错误内容。
因此,需要读取 HTTPURLResponse.statusCode,按照接口约定判断请求是否成功。下面使用常见的 200..<300 范围。
一个可复用的 GET 请求实现
这个版本使用可选 token,检查 URL 和 HTTP 响应,并约定所有 completion handler 都异步回到主线程。
import Foundation
enum APIError: Error {
case invalidURL
case invalidResponse
case httpStatus(Int)
}
final class APIManager {
static let shared = APIManager()
private init() {}
typealias Completion = (Result<Data, Error>) -> Void
func get(
url: String,
token: String? = nil,
completion: @escaping Completion
) {
let finish: Completion = { result in
DispatchQueue.main.async {
completion(result)
}
}
guard let requestURL = URL(string: url),
let scheme = requestURL.scheme?.lowercased(),
["http", "https"].contains(scheme),
requestURL.host != nil else {
finish(.failure(APIError.invalidURL))
return
}
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
request.setValue(
"application/json",
forHTTPHeaderField: "Accept"
)
if let token = token, !token.isEmpty {
request.setValue(
"Bearer \(token)",
forHTTPHeaderField: "Authorization"
)
}
URLSession.shared.dataTask(with: request) {
data, response, error in
if let error = error {
finish(.failure(error))
return
}
guard let response = response as? HTTPURLResponse else {
finish(.failure(APIError.invalidResponse))
return
}
guard (200..<300).contains(response.statusCode) else {
finish(.failure(
APIError.httpStatus(response.statusCode)
))
return
}
finish(.success(data ?? Data()))
}.resume()
}
}
没有响应体的成功响应会返回空 Data,因此不会仅因为 204 等成功响应没有响应体就将其判为失败。如果接口明确要求返回 JSON,应在后续解析时检查响应内容。
示例只将失败状态码传给调用方。如果业务需要读取服务端错误码、错误消息或响应头,可以扩展错误类型或返回类型,保留这些信息。
如何调用?
公开接口不传 token:
APIManager.shared.get(
url: "https://example.com/public"
) { result in
switch result {
case .success(let data):
print("Received \(data.count) bytes")
case .failure(let error):
print("Request failed: \(error)")
}
}
认证接口传入实际 token:
func loadProfile(token: String) {
APIManager.shared.get(
url: "https://example.com/profile",
token: token
) { result in
switch result {
case .success(let data):
print("Received \(data.count) bytes")
case .failure(let error):
print("Request failed: \(error)")
}
}
}
这些 URL 仅用于展示调用方式,需要替换成实际接口地址。
token 和线程还需要注意什么?
- 可选 token 适合这个通用请求方法。对于必须登录的接口,业务层可以提前拒绝缺少 token 的调用,或通过专门的认证接口方法要求传入 token。没有 token 时不添加请求头,并不意味着接口允许匿名访问。
- 本示例使用 Bearer 认证格式。只有服务端采用该格式时,才发送
Authorization: Bearer <token>;其他认证方式应遵循接口约定。 Accept表达期望的响应格式,Content-Type描述发送的请求体格式。原代码没有 GET 请求体,通常不需要声明 JSONContent-Type。- 示例统一在主线程回调,便于调用方更新 UI。较大的 JSON 解码或其他耗时处理应安排在后台,避免直接放进这里的 completion handler。
- 如果 token 需要持久化,iOS 中可使用 Keychain 等适合保存凭证的机制。发送认证请求应使用 HTTPS,也不要把 token 写入日志。
如果以后需要支持更多 HTTP 方法,可以抽出接收 URLRequest 的公共发送方法,让 GET、POST 等方法分别组装请求。当前只有 GET 时,可以先保持接口简单。
备注:内容仅供参考。
