Files
mediaplayer/ios/OralTrainer/Services/HTTPClient.swift
2026-08-17 20:13:28 +08:00

134 lines
4.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
enum HTTPClientError: LocalizedError {
case invalidURL
case badStatus(Int, String)
var errorDescription: String? {
switch self {
case .invalidURL:
return "无效的服务地址"
case .badStatus(let code, let body):
return "HTTP \(code): \(body)"
}
}
}
struct HTTPResult {
let statusCode: Int
let body: Data
let effectiveBaseURL: String
}
enum HTTPClient {
static func getJSON<T: Decodable>(
_ type: T.Type,
baseURL: String,
path: String,
timeout: TimeInterval
) async throws -> (value: T, effectiveBaseURL: String) {
let result = try await execute(
baseURL: baseURL,
path: path,
method: "GET",
timeout: timeout,
headers: ["Accept": "application/json"]
)
guard (200...299).contains(result.statusCode) else {
let body = String(data: result.body, encoding: .utf8) ?? ""
throw HTTPClientError.badStatus(result.statusCode, body)
}
let value = try JSONDecoder().decode(T.self, from: result.body)
return (value, result.effectiveBaseURL)
}
static func execute(
baseURL: String,
path: String,
method: String,
timeout: TimeInterval,
headers: [String: String] = [:],
body: Data? = nil
) async throws -> HTTPResult {
let trimmedBase = baseURL.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
guard let base = URL(string: trimmedBase), base.scheme != nil, base.host != nil else {
throw HTTPClientError.invalidURL
}
let endpoint = URL(string: path, relativeTo: base) ?? base
do {
return try await perform(
url: endpoint,
method: method,
timeout: timeout,
headers: headers,
body: body,
effectiveBaseURL: trimmedBase
)
} catch {
// HTTPS TLS HTTP
guard AppConfig.allowHTTPFallback,
endpoint.scheme == "https",
isTLSError(error) else {
throw error
}
var baseComponents = URLComponents(string: trimmedBase)
baseComponents?.scheme = "http"
guard let httpBase = baseComponents?.string,
let httpURL = URL(string: path, relativeTo: URL(string: httpBase)) else {
throw error
}
return try await perform(
url: httpURL,
method: method,
timeout: timeout,
headers: headers,
body: body,
effectiveBaseURL: httpBase
)
}
}
private static func perform(
url: URL,
method: String,
timeout: TimeInterval,
headers: [String: String],
body: Data?,
effectiveBaseURL: String
) async throws -> HTTPResult {
var request = URLRequest(url: url)
request.httpMethod = method
request.timeoutInterval = timeout
request.setValue(AppConfig.userAgent, forHTTPHeaderField: "User-Agent")
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
request.httpBody = body
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw HTTPClientError.invalidURL
}
return HTTPResult(statusCode: http.statusCode, body: data, effectiveBaseURL: effectiveBaseURL)
}
private static func isTLSError(_ error: Error) -> Bool {
let nsError = error as NSError
let tlsCodes: [Int] = [
NSURLErrorSecureConnectionFailed,
NSURLErrorServerCertificateHasBadDate,
NSURLErrorServerCertificateUntrusted,
NSURLErrorServerCertificateHasUnknownRoot,
NSURLErrorServerCertificateNotYetValid,
NSURLErrorClientCertificateRejected,
NSURLErrorClientCertificateRequired,
NSURLErrorCannotLoadFromNetwork,
]
if tlsCodes.contains(nsError.code) {
return true
}
let message = (nsError.localizedDescription + " " + (nsError.localizedFailureReason ?? ""))
.lowercased()
return message.contains("ssl") || message.contains("tls") || message.contains("certificate")
}
}