diff --git a/Sources/XKit/GrandSlam/GrandSlamClient.swift b/Sources/XKit/GrandSlam/GrandSlamClient.swift index 2c17236b..2a7b23ab 100644 --- a/Sources/XKit/GrandSlam/GrandSlamClient.swift +++ b/Sources/XKit/GrandSlam/GrandSlamClient.swift @@ -41,7 +41,11 @@ struct GrandSlamClient: Sendable { } request.configure(request: &httpRequest, deviceInfo: deviceInfo, anisetteData: anisetteData) - let resp = try await httpClient.makeRequest(httpRequest, body: body) + // GrandSlam doesn't seem to like reused (keep-alive) connections so create a + // new one for each request. cf https://github.com/rileytestut/AltSign/pull/52 + let resp = try await httpClient.withEphemeralClient { + try await $0.makeRequest(httpRequest, body: body) + } return try R.Decoder.decode(data: resp.body) } diff --git a/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift b/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift index eaa8f607..0091980c 100644 --- a/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift +++ b/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift @@ -18,23 +18,31 @@ import OpenAPIAsyncHTTPClient import Dependencies extension HTTPClientDependencyKey: DependencyKey { - public static let liveValue: HTTPClientProtocol = { + public static let liveValue: HTTPClientProtocol = Client() +} + +private struct Client: HTTPClientProtocol { + private static let tlsConfiguration: TLSConfiguration = { // if ssl cert parsing fails we're screwed so we might as well force try // swiftlint:disable:next force_try let appleRootCA = try! NIOSSLCertificate(bytes: Array(appleRootPEM.utf8), format: .pem) var tlsConfiguration: TLSConfiguration = .makeClientConfiguration() tlsConfiguration.additionalTrustRoots = [.certificates([appleRootCA])] + return tlsConfiguration + }() + + var client: HTTPClient + + init() { var config = HTTPClient.Configuration( - tlsConfiguration: tlsConfiguration, + tlsConfiguration: Self.tlsConfiguration, decompression: .enabled(limit: .none) ) config.timeout.connect = .seconds(60) - return HTTPClient(configuration: config) - }() -} + self.client = HTTPClient(configuration: config) + } -extension HTTPClient: HTTPClientProtocol { - public func makeWebSocket(url: URL) async throws -> any WebSocketSession { + func makeWebSocket(url: URL) async throws -> any WebSocketSession { let (stream, continuation) = AsyncStream.makeStream(of: WebSocketSessionWrapper.self) async let value = stream.first(where: { _ in true }) // must be after the `async let` so that we finish if connect throws @@ -42,7 +50,7 @@ extension HTTPClient: HTTPClientProtocol { // we can't use the async overload because we need to immediately subscribe // to onText/onBinary in the same EventLoop tick that the WebSocket is created. // This is also why we create the SessionWrapper inside the closure. - let future = WebSocket.connect(to: url, on: eventLoopGroup) { + let future = WebSocket.connect(to: url, on: client.eventLoopGroup) { continuation.yield(WebSocketSessionWrapper(webSocket: $0)) } try await future.get() @@ -56,8 +64,17 @@ extension HTTPClient: HTTPClientProtocol { case connectFailed } - public var asOpenAPITransport: any ClientTransport { - AsyncHTTPClientTransport(configuration: .init(client: self)) + var asOpenAPITransport: any ClientTransport { + AsyncHTTPClientTransport(configuration: .init(client: client)) + } + + func withEphemeralClient( + perform: (any HTTPClientProtocol) async throws -> T + ) async throws -> T { + let ephemeralClient = Client() + let result = await Result { try await perform(ephemeralClient) } + try? await ephemeralClient.client.shutdown() + return try result.get() } } diff --git a/Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift b/Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift index a8dc6646..503a0036 100644 --- a/Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift +++ b/Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift @@ -18,6 +18,10 @@ public protocol HTTPClientProtocol: Sendable { var asOpenAPITransport: ClientTransport { get } func makeWebSocket(url: URL) async throws -> WebSocketSession + + func withEphemeralClient( + perform: (any HTTPClientProtocol) async throws -> T + ) async throws -> T } extension HTTPClientProtocol { @@ -46,7 +50,7 @@ extension HTTPClientProtocol { ) async throws -> (response: HTTPResponse, body: Data) { await onProgress(0) let (response, responseBody) = try await send(request, body: body.map { HTTPBody($0) }) - guard !requireHTTPSuccess || response.status.kind == .successful else { + guard !requireHTTPSuccess || ![.clientError, .serverError].contains(response.status.kind) else { let errorBody = (try? await responseBody.collect()) ?? Data() throw HTTPResponseError( method: request.method, @@ -128,6 +132,12 @@ private struct UnimplementedHTTPClient: HTTPClientProtocol, ClientTransport { return try closure() } + func withEphemeralClient( + 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) diff --git a/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift b/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift index 74527bfc..1e94f948 100644 --- a/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift +++ b/Sources/XKit/HTTPClientProtocol/URLSession+HTTP.swift @@ -56,6 +56,15 @@ private struct Client: HTTPClientProtocol { URLSessionTransport(configuration: .init(session: session)) } + func withEphemeralClient( + perform: (any HTTPClientProtocol) async throws -> T + ) async throws -> T { + let ephemeralClient = Client() + let result = await Result { try await perform(ephemeralClient) } + ephemeralClient.session.finishTasksAndInvalidate() + return try result.get() + } + public func makeWebSocket(url: URL) async throws -> any WebSocketSession { let task = session.webSocketTask(with: url) let (event, eventContinuation) = AsyncStream.makeStream()