added ios module

This commit is contained in:
2026-08-17 20:13:28 +08:00
parent a8031e2e3b
commit 7c81976b2b
27 changed files with 2767 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
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")
}
}