Files
2026-07-13 12:22:33 +08:00

132 lines
5.2 KiB
Swift

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
extension GeminiStatusProbe {
public static func defaultDataLoader(for request: URLRequest) async throws -> (Data, URLResponse) {
let loader = Self.dataLoaderWithCurlFallback(
primary: { request in
try await ProviderHTTPClient.shared.data(for: request)
},
fallback: Self.curlDataLoader)
return try await loader(request)
}
public static func dataLoaderWithCurlFallback(
primary: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse),
fallback: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse))
-> @Sendable (URLRequest) async throws -> (Data, URLResponse)
{
{ request in
do {
return try await primary(request)
} catch {
guard Self.isURLSessionTimeout(error) else {
throw error
}
CodexBarLog.logger(LogCategories.geminiProbe)
.warning("Gemini URLSession timed out; retrying with curl")
return try await fallback(request)
}
}
}
private static func isURLSessionTimeout(_ error: Error) -> Bool {
if let urlError = error as? URLError {
return urlError.code == .timedOut
}
let nsError = error as NSError
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut
}
private static func curlDataLoader(for request: URLRequest) async throws -> (Data, URLResponse) {
guard let url = request.url else {
throw URLError(.badURL)
}
let fileManager = FileManager.default
let tempDir = fileManager.temporaryDirectory
.appendingPathComponent("codexbar-gemini-curl-\(UUID().uuidString)", isDirectory: true)
try fileManager.createDirectory(
at: tempDir,
withIntermediateDirectories: true,
attributes: [.posixPermissions: 0o700])
defer {
try? fileManager.removeItem(at: tempDir)
}
let configURL = tempDir.appendingPathComponent("curl.conf")
var config = [
"silent",
"show-error",
"location",
"url = \(Self.curlConfigQuote(url.absoluteString))",
"max-time = \(max(1, Int(ceil(request.timeoutInterval))))",
]
if let method = request.httpMethod, !method.isEmpty {
config.append("request = \(Self.curlConfigQuote(method))")
}
for (name, value) in (request.allHTTPHeaderFields ?? [:]).sorted(by: { $0.key < $1.key }) {
let header = "\(name): \(value)"
guard !header.contains("\n"), !header.contains("\r") else {
throw GeminiStatusProbeError.apiError("Invalid request header")
}
config.append("header = \(Self.curlConfigQuote(header))")
}
if let body = request.httpBody {
let bodyURL = tempDir.appendingPathComponent("body")
try body.write(to: bodyURL, options: .atomic)
try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: bodyURL.path)
config.append("data-binary = \(Self.curlConfigQuote("@\(bodyURL.path)"))")
}
config.append("write-out = \(Self.curlConfigQuote(Self.curlHTTPStatusMarker + "%{http_code}"))")
try config.joined(separator: "\n").write(to: configURL, atomically: true, encoding: .utf8)
try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: configURL.path)
let result = try await SubprocessRunner.run(
binary: "/usr/bin/curl",
arguments: ["--config", configURL.path],
environment: TTYCommandRunner.enrichedEnvironment(),
timeout: max(5, request.timeoutInterval + 2),
label: "gemini-api-curl")
return try Self.parseCurlDataLoaderResult(result.stdout, url: url)
}
private static let curlHTTPStatusMarker = "__CODEXBAR_HTTP_STATUS__:"
private static func curlConfigQuote(_ value: String) -> String {
let escaped = value
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
return "\"\(escaped)\""
}
private static func parseCurlDataLoaderResult(_ output: String, url: URL) throws -> (Data, URLResponse) {
guard let markerRange = output.range(of: curlHTTPStatusMarker, options: .backwards) else {
throw GeminiStatusProbeError.apiError("curl response missing HTTP status")
}
let body = String(output[..<markerRange.lowerBound])
let statusText = output[markerRange.upperBound...]
.trimmingCharacters(in: .whitespacesAndNewlines)
guard let statusCode = Int(statusText), statusCode > 0,
let response = HTTPURLResponse(
url: url,
statusCode: statusCode,
httpVersion: nil,
headerFields: nil)
else {
throw GeminiStatusProbeError.apiError("curl response had invalid HTTP status")
}
return (Data(body.utf8), response)
}
}