From 86e47f11277179240f0b3b61f830ecfe1ccae60c Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Tue, 8 Sep 2026 20:57:07 -0400 Subject: [PATCH 1/3] gsa: disregard keepalive --- Sources/XKit/GrandSlam/GrandSlamClient.swift | 6 ++- .../AsyncHTTPClient+HTTP.swift | 37 ++++++++++++++----- .../HTTPClientProtocol.swift | 14 ++++++- .../HTTPClientProtocol/URLSession+HTTP.swift | 9 +++++ 4 files changed, 53 insertions(+), 13 deletions(-) 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..aaa4b1b6 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..a4750576 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 { @@ -41,12 +45,12 @@ extension HTTPClientProtocol { public func makeRequest( _ request: HTTPRequest, body: Data? = nil, - requireHTTPSuccess: Bool = true, + throwOnServerError: 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 || response.status.kind == .successful else { + guard !throwOnServerError || response.status.kind != .serverError 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() From 19472b53b0ed69f5427df99c7e3faba1683525f5 Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Tue, 8 Sep 2026 21:03:03 -0400 Subject: [PATCH 2/3] Lint --- Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift b/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift index aaa4b1b6..0091980c 100644 --- a/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift +++ b/Sources/XKit/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift @@ -30,7 +30,7 @@ private struct Client: HTTPClientProtocol { tlsConfiguration.additionalTrustRoots = [.certificates([appleRootCA])] return tlsConfiguration }() - + var client: HTTPClient init() { From 53ac580286699cc8a12d1d17a70ce42bc4fccfff Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Tue, 8 Sep 2026 21:10:12 -0400 Subject: [PATCH 3/3] Handle 4xx too --- Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift b/Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift index a4750576..503a0036 100644 --- a/Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift +++ b/Sources/XKit/HTTPClientProtocol/HTTPClientProtocol.swift @@ -45,12 +45,12 @@ extension HTTPClientProtocol { public func makeRequest( _ request: HTTPRequest, body: Data? = nil, - throwOnServerError: Bool = true, + 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 !throwOnServerError || response.status.kind != .serverError else { + guard !requireHTTPSuccess || ![.clientError, .serverError].contains(response.status.kind) else { let errorBody = (try? await responseBody.collect()) ?? Data() throw HTTPResponseError( method: request.method,