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( _ 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") } }