mirror of
https://github.com/xtool-org/xtool.git
synced 2026-10-25 09:15:01 +01:00
GSA seems to, sometimes, reject HTTP requests made on a kept-alive connection with a 503. This was either the real root cause, or a second root cause, for #243. Fix this by creating a new HTTP connection for each GSA request. See also: https://github.com/rileytestut/AltSign/pull/52 and https://github.com/rileytestut/AltSign/pull/47 Speculatively fixes #232
168 lines
5.3 KiB
Swift
168 lines
5.3 KiB
Swift
//
|
|
// HTTPClientProtocol.swift
|
|
//
|
|
//
|
|
// Created by Kabir Oberai on 05/05/21.
|
|
//
|
|
|
|
import Foundation
|
|
import OpenAPIRuntime
|
|
import Dependencies
|
|
import HTTPTypes
|
|
|
|
public typealias HTTPRequest = HTTPTypes.HTTPRequest
|
|
public typealias HTTPResponse = HTTPTypes.HTTPResponse
|
|
public typealias HTTPBody = OpenAPIRuntime.HTTPBody
|
|
|
|
public protocol HTTPClientProtocol: Sendable {
|
|
var asOpenAPITransport: ClientTransport { get }
|
|
|
|
func makeWebSocket(url: URL) async throws -> WebSocketSession
|
|
|
|
func withEphemeralClient<T>(
|
|
perform: (any HTTPClientProtocol) async throws -> T
|
|
) async throws -> T
|
|
}
|
|
|
|
extension HTTPClientProtocol {
|
|
public func send(
|
|
_ request: HTTPRequest,
|
|
body: HTTPBody? = nil
|
|
) async throws -> (response: HTTPResponse, body: HTTPBody?) {
|
|
let transport = asOpenAPITransport
|
|
let request = request
|
|
var baseComponents = URLComponents()
|
|
baseComponents.scheme = request.scheme
|
|
baseComponents.host = request.authority
|
|
return try await transport.send(
|
|
request,
|
|
body: body,
|
|
baseURL: baseComponents.url!,
|
|
operationID: "dummy"
|
|
)
|
|
}
|
|
|
|
public func makeRequest(
|
|
_ request: HTTPRequest,
|
|
body: Data? = nil,
|
|
requireHTTPSuccess: Bool = true,
|
|
onProgress: @isolated(any) (Double?) -> Void = { _ in }
|
|
) async throws -> (response: HTTPResponse, body: Data) {
|
|
await onProgress(0)
|
|
let (response, responseBody) = try await send(request, body: body.map { HTTPBody($0) })
|
|
guard !requireHTTPSuccess || ![.clientError, .serverError].contains(response.status.kind) else {
|
|
let errorBody = (try? await responseBody.collect()) ?? Data()
|
|
throw HTTPResponseError(
|
|
method: request.method,
|
|
url: "\(request.scheme ?? "https")://\(request.authority ?? "")\(request.path ?? "")",
|
|
status: response.status,
|
|
// we don't use `decoding:as:` because we want to validate the UTF8
|
|
body: String(data: errorBody, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
)
|
|
}
|
|
return (response, try await responseBody.collect(onProgress: onProgress))
|
|
}
|
|
}
|
|
|
|
extension HTTPBody? {
|
|
fileprivate func collect(
|
|
onProgress: @isolated(any) (Double?) -> Void = { _ in }
|
|
) async throws -> Data {
|
|
try await self?.collect(onProgress: onProgress) ?? Data()
|
|
}
|
|
}
|
|
|
|
extension HTTPBody {
|
|
fileprivate func collect(
|
|
onProgress: @isolated(any) (Double?) -> Void
|
|
) async throws -> Data {
|
|
switch self.length {
|
|
case .unknown:
|
|
return try await self.reduce(into: Data()) { $0 += $1 }
|
|
case .known(let length):
|
|
var data = Data(capacity: Int(length))
|
|
let total = Double(length)
|
|
for try await chunk in self {
|
|
data += chunk
|
|
await onProgress(Swift.min(Double(data.count) / total, 1))
|
|
}
|
|
return data
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct HTTPResponseError: Error, LocalizedError, Sendable, CustomStringConvertible {
|
|
public let method: HTTPRequest.Method
|
|
public let url: String
|
|
public let status: HTTPResponse.Status
|
|
public let body: String?
|
|
|
|
public var description: String {
|
|
var description = "\(method.rawValue) \(url) failed: HTTP \(status.code)"
|
|
if !status.reasonPhrase.isEmpty {
|
|
description += " \(status.reasonPhrase)"
|
|
}
|
|
if let body, !body.isEmpty {
|
|
description += ". Details:\n\(body)"
|
|
}
|
|
return description
|
|
}
|
|
|
|
public var errorDescription: String? { description }
|
|
}
|
|
|
|
private struct UnimplementedHTTPClient: HTTPClientProtocol, ClientTransport {
|
|
public var asOpenAPITransport: ClientTransport { self }
|
|
|
|
func send(
|
|
_ request: HTTPRequest,
|
|
body: HTTPBody?,
|
|
baseURL: URL,
|
|
operationID: String
|
|
) async throws -> (HTTPResponse, HTTPBody?) {
|
|
let closure: (HTTPRequest, HTTPBody?, URL, String) async throws -> (HTTPResponse, HTTPBody?) = unimplemented()
|
|
return try await closure(request, body, baseURL, operationID)
|
|
}
|
|
|
|
func makeRequest(
|
|
_ request: HTTPRequest,
|
|
onProgress: sending @isolated(any) (Double?) -> Void
|
|
) async throws -> HTTPResponse {
|
|
let closure: () throws -> HTTPResponse = unimplemented()
|
|
return try closure()
|
|
}
|
|
|
|
func withEphemeralClient<T>(
|
|
perform: (any HTTPClientProtocol) async throws -> T
|
|
) async throws -> T {
|
|
try await perform(self)
|
|
}
|
|
|
|
public func makeWebSocket(url: URL) async throws -> any WebSocketSession {
|
|
let closure: (URL) async throws -> any WebSocketSession = unimplemented()
|
|
return try await closure(url)
|
|
}
|
|
}
|
|
|
|
public enum HTTPClientDependencyKey: TestDependencyKey {
|
|
public static let testValue: HTTPClientProtocol = UnimplementedHTTPClient()
|
|
}
|
|
|
|
extension DependencyValues {
|
|
public var httpClient: HTTPClientProtocol {
|
|
get { self[HTTPClientDependencyKey.self] }
|
|
set { self[HTTPClientDependencyKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
public protocol WebSocketSession: Sendable {
|
|
func receive() async throws -> WebSocketMessage
|
|
func send(_ message: WebSocketMessage) async throws
|
|
func close()
|
|
}
|
|
|
|
public enum WebSocketMessage: Sendable {
|
|
case text(String)
|
|
case data(Data)
|
|
}
|