From 6d5f37fb23b8d9d9fd3113dc18ac0127edebd99e Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 13 Feb 2026 10:55:02 -0700 Subject: [PATCH 1/6] feat!: Support GraphQLTransportWS & GraphQLWS v1 --- Examples/HelloWorld/Package.resolved | 10 ++--- Package.resolved | 10 ++--- Package.swift | 4 +- Sources/GraphQLVapor/GraphQLConfig.swift | 9 ++-- Sources/GraphQLVapor/GraphQLHandler.swift | 5 ++- .../GraphQLVapor/RoutesBuilder+graphql.swift | 7 ++-- .../GraphQLHandler+handleWebSocket.swift | 42 +++++++++++++++---- .../WebSocket/WebSocketMessenger.swift | 32 +------------- 8 files changed, 61 insertions(+), 58 deletions(-) diff --git a/Examples/HelloWorld/Package.resolved b/Examples/HelloWorld/Package.resolved index e4c95ee..9999fca 100644 --- a/Examples/HelloWorld/Package.resolved +++ b/Examples/HelloWorld/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "8f6979a63f7f2c6f9ab39ce5da38c841b974c0f25ae6099c5f6e157e852dbc05", + "originHash" : "24cbfc68b865463dd46c318af906571b41ff169eaa9848fbf6c71f2b064530bb", "pins" : [ { "identity" : "async-http-client", @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/GraphQLSwift/GraphQLTransportWS.git", "state" : { - "revision" : "43a64cbec3177d05e834b9a70051ab858744b0d3", - "version" : "0.2.1" + "revision" : "6d62d9f980346f0644a5f4dc1593f2b0f1d07b44", + "version" : "1.0.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/GraphQLSwift/GraphQLWS.git", "state" : { - "revision" : "66b2570775436ce83f048cb8532a2baa9cbc7540", - "version" : "0.2.1" + "revision" : "5a3b62e478668199d22f52df7cc7d44b51394cb4", + "version" : "1.0.0" } }, { diff --git a/Package.resolved b/Package.resolved index 9c29d4f..c8aa9ab 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d16e280d13daf81824e54056378363b4a23707796251c37a2d8641976cfa0477", + "originHash" : "78b8062b4c6f9e751b88d65f6b57fb80012daef3bfd81d95874b910c9ce8c7d7", "pins" : [ { "identity" : "async-http-client", @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/GraphQLSwift/GraphQLTransportWS.git", "state" : { - "revision" : "43a64cbec3177d05e834b9a70051ab858744b0d3", - "version" : "0.2.1" + "revision" : "6d62d9f980346f0644a5f4dc1593f2b0f1d07b44", + "version" : "1.0.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/GraphQLSwift/GraphQLWS.git", "state" : { - "revision" : "66b2570775436ce83f048cb8532a2baa9cbc7540", - "version" : "0.2.1" + "revision" : "5a3b62e478668199d22f52df7cc7d44b51394cb4", + "version" : "1.0.0" } }, { diff --git a/Package.swift b/Package.swift index 0f4bf17..aec5449 100644 --- a/Package.swift +++ b/Package.swift @@ -15,8 +15,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "4.0.0"), - .package(url: "https://github.com/GraphQLSwift/GraphQLTransportWS.git", from: "0.2.1"), - .package(url: "https://github.com/GraphQLSwift/GraphQLWS.git", from: "0.2.1"), + .package(url: "https://github.com/GraphQLSwift/GraphQLTransportWS.git", from: "1.0.0"), + .package(url: "https://github.com/GraphQLSwift/GraphQLWS.git", from: "1.0.0"), .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), ], targets: [ diff --git a/Sources/GraphQLVapor/GraphQLConfig.swift b/Sources/GraphQLVapor/GraphQLConfig.swift index 1102c06..a1302f3 100644 --- a/Sources/GraphQLVapor/GraphQLConfig.swift +++ b/Sources/GraphQLVapor/GraphQLConfig.swift @@ -1,7 +1,10 @@ import GraphQL /// Configuration options for GraphQLVapor -public struct GraphQLConfig: Sendable { +public struct GraphQLConfig< + WebSocketInit: Equatable & Codable & Sendable, + WebSocketInitResult: Sendable +>: Sendable { let allowGet: Bool let allowMissingAcceptHeader: Bool let ide: IDE @@ -67,14 +70,14 @@ public struct GraphQLConfig: Send } public struct WebSocket: Sendable { - let onWebsocketInit: @Sendable (WebSocketInit) async throws -> Void + let onWebsocketInit: @Sendable (WebSocketInit) async throws -> WebSocketInitResult /// GraphQL over WebSocket configuration /// - Parameter onWebsocketInit: A custom callback run during `connection_init` resolution that allows /// authorization using the `payload` field of the `connection_init` message. /// Throw from this closure to indicate that authorization has failed. public init( - onWebsocketInit: @Sendable @escaping (WebSocketInit) async throws -> Void = { (_: EmptyWebsocketInit) in } + onWebsocketInit: @Sendable @escaping (WebSocketInit) async throws -> WebSocketInitResult = { (_: EmptyWebsocketInit) in } ) { self.onWebsocketInit = onWebsocketInit } diff --git a/Sources/GraphQLVapor/GraphQLHandler.swift b/Sources/GraphQLVapor/GraphQLHandler.swift index 16b28d6..bc7b639 100644 --- a/Sources/GraphQLVapor/GraphQLHandler.swift +++ b/Sources/GraphQLVapor/GraphQLHandler.swift @@ -3,10 +3,11 @@ import Vapor struct GraphQLHandler< Context: Sendable, - WebSocketInit: Equatable & Codable & Sendable + WebSocketInit: Equatable & Codable & Sendable, + WebSocketInitResult: Sendable >: Sendable { let schema: GraphQLSchema let rootValue: any Sendable - let config: GraphQLConfig + let config: GraphQLConfig let computeContext: @Sendable (GraphQLContextComputationInputs) async throws -> Context } diff --git a/Sources/GraphQLVapor/RoutesBuilder+graphql.swift b/Sources/GraphQLVapor/RoutesBuilder+graphql.swift index 43dcd8d..9569a6a 100644 --- a/Sources/GraphQLVapor/RoutesBuilder+graphql.swift +++ b/Sources/GraphQLVapor/RoutesBuilder+graphql.swift @@ -20,19 +20,20 @@ public extension RoutesBuilder { /// - computeContext: A closure used to compute the GraphQL context from incoming requests. This must be provided. func graphql< Context: Sendable, - WebSocketInit: Equatable & Codable & Sendable + WebSocketInit: Equatable & Codable & Sendable, + WebSocketInitResult: Sendable >( _ path: [PathComponent] = ["graphql"], schema: GraphQLSchema, rootValue: any Sendable = (), - config: GraphQLConfig = GraphQLConfig(), + config: GraphQLConfig = GraphQLConfig(), computeContext: @Sendable @escaping (GraphQLContextComputationInputs) async throws -> Context ) { ContentConfiguration.global.use(encoder: GraphQLJSONEncoder(), for: .jsonGraphQL) ContentConfiguration.global.use(decoder: JSONDecoder(), for: .jsonGraphQL) // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#request - let handler = GraphQLHandler(schema: schema, rootValue: rootValue, config: config, computeContext: computeContext) + let handler = GraphQLHandler(schema: schema, rootValue: rootValue, config: config, computeContext: computeContext) get(path) { request in // WebSocket handling if diff --git a/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift b/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift index 583c56c..97af0db 100644 --- a/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift +++ b/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift @@ -18,13 +18,30 @@ extension GraphQLHandler { } }, onUpgrade: { websocket in + let messageStream = AsyncThrowingStream { continuation in + websocket.onText { _, text in + continuation.yield(text) + } + websocket.onClose.whenComplete { result in + switch result { + case .success: + continuation.finish() + case let .failure(error): + continuation.finish(throwing: error) + } + } + } + let messenger = WebSocketMessenger(websocket: websocket) switch subProtocol { case .graphqlTransportWs: // https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md - let server = GraphQLTransportWS.Server>( + let server = GraphQLTransportWS.Server>( messenger: messenger, - onExecute: { graphQLRequest in + onInit: { initPayload in + try await config.websocket.onWebsocketInit(initPayload) + }, + onExecute: { graphQLRequest, _ in let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, graphQLRequest: graphQLRequest @@ -39,7 +56,7 @@ extension GraphQLHandler { operationName: graphQLRequest.operationName ) }, - onSubscribe: { graphQLRequest in + onSubscribe: { graphQLRequest, _ in let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, graphQLRequest: graphQLRequest @@ -55,12 +72,18 @@ extension GraphQLHandler { ).get() } ) - server.auth(config.websocket.onWebsocketInit) + Task { + // This task completes upon websocket closure + try await server.listen(to: messageStream) + } case .graphqlWs: // https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md - let server = GraphQLWS.Server>( + let server = GraphQLWS.Server>( messenger: messenger, - onExecute: { graphQLRequest in + onInit: { initPayload in + try await config.websocket.onWebsocketInit(initPayload) + }, + onExecute: { graphQLRequest, _ in let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, graphQLRequest: graphQLRequest @@ -75,7 +98,7 @@ extension GraphQLHandler { operationName: graphQLRequest.operationName ) }, - onSubscribe: { graphQLRequest in + onSubscribe: { graphQLRequest, _ in let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, graphQLRequest: graphQLRequest @@ -91,7 +114,10 @@ extension GraphQLHandler { ).get() } ) - server.auth(config.websocket.onWebsocketInit) + Task { + // This task completes upon websocket closure + try await server.listen(to: messageStream) + } } } ) diff --git a/Sources/GraphQLVapor/WebSocket/WebSocketMessenger.swift b/Sources/GraphQLVapor/WebSocket/WebSocketMessenger.swift index 094b5d2..e585aa8 100644 --- a/Sources/GraphQLVapor/WebSocket/WebSocketMessenger.swift +++ b/Sources/GraphQLVapor/WebSocket/WebSocketMessenger.swift @@ -3,47 +3,19 @@ import GraphQLWS import WebSocketKit /// Messenger wrapper for WebSockets -class WebSocketMessenger: GraphQLTransportWS.Messenger, GraphQLWS.Messenger, @unchecked Sendable { - private weak var websocket: WebSocket? - private var onReceive: (String) async throws -> Void = { _ in } - - init(websocket: WebSocket) { - self.websocket = websocket - websocket.onText { _, message in - // We must include self here, without a weak reference to prevent it from falling - // out of scope while the websocket is still alive - do { - try await self.onReceive(message) - } catch { - try? await self.error("\(error)", code: 4400) - } - } - websocket.onClose.whenComplete { [weak self] _ in - guard let self = self else { - return - } - self.onReceive { _ in } - websocket.onText { _, _ in } - } - } +struct WebSocketMessenger: GraphQLTransportWS.Messenger, GraphQLWS.Messenger { + let websocket: WebSocket func send(_ message: S) async throws where S.Element == Character { - guard let websocket = websocket else { return } try await websocket.send(message) } - func onReceive(callback: @escaping (String) async throws -> Void) { - onReceive = callback - } - func error(_ message: String, code: Int) async throws { - guard let websocket = websocket else { return } try await websocket.send("\(code): \(message)") try await websocket.close(code: .init(codeNumber: code)) } func close() async throws { - guard let websocket = websocket else { return } try await websocket.close() } } From ce69134131bf5a49cb0c10f65805c8cf7ed1354e Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 13 Feb 2026 10:58:56 -0700 Subject: [PATCH 2/6] feat!: Includes WebSocketInitResult in context computation inputs --- Sources/GraphQLVapor/GraphQLHandler.swift | 2 +- .../HTTP/GraphQLHandler+HTTP.swift | 10 ++++++---- .../GraphQLVapor/RoutesBuilder+graphql.swift | 7 +++++-- .../GraphQLHandler+handleWebSocket.swift | 20 +++++++++++-------- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/Sources/GraphQLVapor/GraphQLHandler.swift b/Sources/GraphQLVapor/GraphQLHandler.swift index bc7b639..917a881 100644 --- a/Sources/GraphQLVapor/GraphQLHandler.swift +++ b/Sources/GraphQLVapor/GraphQLHandler.swift @@ -9,5 +9,5 @@ struct GraphQLHandler< let schema: GraphQLSchema let rootValue: any Sendable let config: GraphQLConfig - let computeContext: @Sendable (GraphQLContextComputationInputs) async throws -> Context + let computeContext: @Sendable (GraphQLContextComputationInputs) async throws -> Context } diff --git a/Sources/GraphQLVapor/HTTP/GraphQLHandler+HTTP.swift b/Sources/GraphQLVapor/HTTP/GraphQLHandler+HTTP.swift index fa0262b..c276465 100644 --- a/Sources/GraphQLVapor/HTTP/GraphQLHandler+HTTP.swift +++ b/Sources/GraphQLVapor/HTTP/GraphQLHandler+HTTP.swift @@ -15,9 +15,10 @@ extension GraphQLHandler { guard operationType != .mutation else { throw Abort(.methodNotAllowed, reason: "Mutations using GET are disallowed") } - let graphQLContextComputationInputs = GraphQLContextComputationInputs( + let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, - graphQLRequest: graphQLRequest + graphQLRequest: graphQLRequest, + websocketInitResult: nil ) let context = try await computeContext(graphQLContextComputationInputs) let result = await execute( @@ -34,9 +35,10 @@ extension GraphQLHandler { throw Abort(.unsupportedMediaType, reason: "Missing `Content-Type` header") } let graphQLRequest = try request.content.decode(GraphQLRequest.self) - let graphQLContextComputationInputs = GraphQLContextComputationInputs( + let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, - graphQLRequest: graphQLRequest + graphQLRequest: graphQLRequest, + websocketInitResult: nil ) let context = try await computeContext(graphQLContextComputationInputs) let result = await execute( diff --git a/Sources/GraphQLVapor/RoutesBuilder+graphql.swift b/Sources/GraphQLVapor/RoutesBuilder+graphql.swift index 9569a6a..fcd19ca 100644 --- a/Sources/GraphQLVapor/RoutesBuilder+graphql.swift +++ b/Sources/GraphQLVapor/RoutesBuilder+graphql.swift @@ -27,7 +27,7 @@ public extension RoutesBuilder { schema: GraphQLSchema, rootValue: any Sendable = (), config: GraphQLConfig = GraphQLConfig(), - computeContext: @Sendable @escaping (GraphQLContextComputationInputs) async throws -> Context + computeContext: @Sendable @escaping (GraphQLContextComputationInputs) async throws -> Context ) { ContentConfiguration.global.use(encoder: GraphQLJSONEncoder(), for: .jsonGraphQL) ContentConfiguration.global.use(decoder: JSONDecoder(), for: .jsonGraphQL) @@ -69,7 +69,10 @@ public extension RoutesBuilder { } } -public struct GraphQLContextComputationInputs: Sendable { +public struct GraphQLContextComputationInputs< + WebSocketInitResult: Sendable +>: Sendable { public let vaporRequest: Request public let graphQLRequest: GraphQLRequest + public let websocketInitResult: WebSocketInitResult? } diff --git a/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift b/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift index 97af0db..5c1848b 100644 --- a/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift +++ b/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift @@ -41,10 +41,11 @@ extension GraphQLHandler { onInit: { initPayload in try await config.websocket.onWebsocketInit(initPayload) }, - onExecute: { graphQLRequest, _ in + onExecute: { graphQLRequest, initResult in let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, - graphQLRequest: graphQLRequest + graphQLRequest: graphQLRequest, + websocketInitResult: initResult ) let context = try await computeContext(graphQLContextComputationInputs) return try await graphql( @@ -56,10 +57,11 @@ extension GraphQLHandler { operationName: graphQLRequest.operationName ) }, - onSubscribe: { graphQLRequest, _ in + onSubscribe: { graphQLRequest, initResult in let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, - graphQLRequest: graphQLRequest + graphQLRequest: graphQLRequest, + websocketInitResult: initResult ) let context = try await computeContext(graphQLContextComputationInputs) return try await graphqlSubscribe( @@ -83,10 +85,11 @@ extension GraphQLHandler { onInit: { initPayload in try await config.websocket.onWebsocketInit(initPayload) }, - onExecute: { graphQLRequest, _ in + onExecute: { graphQLRequest, initResult in let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, - graphQLRequest: graphQLRequest + graphQLRequest: graphQLRequest, + websocketInitResult: initResult ) let context = try await computeContext(graphQLContextComputationInputs) return try await graphql( @@ -98,10 +101,11 @@ extension GraphQLHandler { operationName: graphQLRequest.operationName ) }, - onSubscribe: { graphQLRequest, _ in + onSubscribe: { graphQLRequest, initResult in let graphQLContextComputationInputs = GraphQLContextComputationInputs( vaporRequest: request, - graphQLRequest: graphQLRequest + graphQLRequest: graphQLRequest, + websocketInitResult: initResult ) let context = try await computeContext(graphQLContextComputationInputs) return try await graphqlSubscribe( From 71b00cfba91b157426c358200b13bb270500573c Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 13 Feb 2026 11:00:14 -0700 Subject: [PATCH 3/6] refactor!: Consistent WebSocket capitalization --- Sources/GraphQLVapor/GraphQLConfig.swift | 10 +++++----- Sources/GraphQLVapor/RoutesBuilder+graphql.swift | 2 +- .../GraphQLVapor/WebSocket/EmptyWebSocketInit.swift | 2 +- .../WebSocket/GraphQLHandler+handleWebSocket.swift | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/GraphQLVapor/GraphQLConfig.swift b/Sources/GraphQLVapor/GraphQLConfig.swift index a1302f3..d080154 100644 --- a/Sources/GraphQLVapor/GraphQLConfig.swift +++ b/Sources/GraphQLVapor/GraphQLConfig.swift @@ -27,7 +27,7 @@ public struct GraphQLConfig< subscriptionProtocols: Set = [], websocket: WebSocket = .init( // Including this strongly-typed argument is required to avoid compiler failures on Swift 6.2.3. - onWebsocketInit: { (_: EmptyWebsocketInit) in } + onWebSocketInit: { (_: EmptyWebSocketInit) in } ), additionalValidationRules: [@Sendable (ValidationContext) -> Visitor] = [] ) { @@ -70,16 +70,16 @@ public struct GraphQLConfig< } public struct WebSocket: Sendable { - let onWebsocketInit: @Sendable (WebSocketInit) async throws -> WebSocketInitResult + let onWebSocketInit: @Sendable (WebSocketInit) async throws -> WebSocketInitResult /// GraphQL over WebSocket configuration - /// - Parameter onWebsocketInit: A custom callback run during `connection_init` resolution that allows + /// - Parameter onWebSocketInit: A custom callback run during `connection_init` resolution that allows /// authorization using the `payload` field of the `connection_init` message. /// Throw from this closure to indicate that authorization has failed. public init( - onWebsocketInit: @Sendable @escaping (WebSocketInit) async throws -> WebSocketInitResult = { (_: EmptyWebsocketInit) in } + onWebSocketInit: @Sendable @escaping (WebSocketInit) async throws -> WebSocketInitResult = { (_: EmptyWebSocketInit) in } ) { - self.onWebsocketInit = onWebsocketInit + self.onWebSocketInit = onWebSocketInit } } } diff --git a/Sources/GraphQLVapor/RoutesBuilder+graphql.swift b/Sources/GraphQLVapor/RoutesBuilder+graphql.swift index fcd19ca..84c1bf5 100644 --- a/Sources/GraphQLVapor/RoutesBuilder+graphql.swift +++ b/Sources/GraphQLVapor/RoutesBuilder+graphql.swift @@ -26,7 +26,7 @@ public extension RoutesBuilder { _ path: [PathComponent] = ["graphql"], schema: GraphQLSchema, rootValue: any Sendable = (), - config: GraphQLConfig = GraphQLConfig(), + config: GraphQLConfig = GraphQLConfig(), computeContext: @Sendable @escaping (GraphQLContextComputationInputs) async throws -> Context ) { ContentConfiguration.global.use(encoder: GraphQLJSONEncoder(), for: .jsonGraphQL) diff --git a/Sources/GraphQLVapor/WebSocket/EmptyWebSocketInit.swift b/Sources/GraphQLVapor/WebSocket/EmptyWebSocketInit.swift index ce23cb8..fcefb70 100644 --- a/Sources/GraphQLVapor/WebSocket/EmptyWebSocketInit.swift +++ b/Sources/GraphQLVapor/WebSocket/EmptyWebSocketInit.swift @@ -1 +1 @@ -public struct EmptyWebsocketInit: Equatable, Codable, Sendable {} +public struct EmptyWebSocketInit: Equatable, Codable, Sendable {} diff --git a/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift b/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift index 5c1848b..216ba33 100644 --- a/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift +++ b/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift @@ -39,7 +39,7 @@ extension GraphQLHandler { let server = GraphQLTransportWS.Server>( messenger: messenger, onInit: { initPayload in - try await config.websocket.onWebsocketInit(initPayload) + try await config.websocket.onWebSocketInit(initPayload) }, onExecute: { graphQLRequest, initResult in let graphQLContextComputationInputs = GraphQLContextComputationInputs( @@ -83,7 +83,7 @@ extension GraphQLHandler { let server = GraphQLWS.Server>( messenger: messenger, onInit: { initPayload in - try await config.websocket.onWebsocketInit(initPayload) + try await config.websocket.onWebSocketInit(initPayload) }, onExecute: { graphQLRequest, initResult in let graphQLContextComputationInputs = GraphQLContextComputationInputs( From 4cd80560eb12b6c57ae5cecb22b3b2969810fd5e Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 13 Feb 2026 11:09:07 -0700 Subject: [PATCH 4/6] docs: Documents `GraphQLContextComputationInputs` --- Sources/GraphQLVapor/RoutesBuilder+graphql.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/GraphQLVapor/RoutesBuilder+graphql.swift b/Sources/GraphQLVapor/RoutesBuilder+graphql.swift index 84c1bf5..673857e 100644 --- a/Sources/GraphQLVapor/RoutesBuilder+graphql.swift +++ b/Sources/GraphQLVapor/RoutesBuilder+graphql.swift @@ -69,10 +69,17 @@ public extension RoutesBuilder { } } +/// Request metadata that can be used to construct a GraphQL context public struct GraphQLContextComputationInputs< WebSocketInitResult: Sendable >: Sendable { + /// The Vapor request that initiated the GraphQL request. In WebSockets, this is the upgrade GET request. public let vaporRequest: Request + + /// The decoded GraphQL request, including the raw query, variables, and more public let graphQLRequest: GraphQLRequest + + /// The result of the WebSocket's initialization closure. This can be used to customize GraphQL context creation based on the init + /// message metadata as opposed to only the upgrade request. In non-WebSocket contexts, this is nil. public let websocketInitResult: WebSocketInitResult? } From 1cdb507153bb5de133fc9ec09db3a407fe58e8e1 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 13 Feb 2026 12:01:23 -0700 Subject: [PATCH 5/6] feat!: `onWebSocketInit` gets access to Request This will give access to services (like a database or HTTPClient) in order to validate the init message and do things like auth checks. --- Sources/GraphQLVapor/GraphQLConfig.swift | 7 ++++--- .../WebSocket/GraphQLHandler+handleWebSocket.swift | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/GraphQLVapor/GraphQLConfig.swift b/Sources/GraphQLVapor/GraphQLConfig.swift index d080154..89bfcb8 100644 --- a/Sources/GraphQLVapor/GraphQLConfig.swift +++ b/Sources/GraphQLVapor/GraphQLConfig.swift @@ -1,4 +1,5 @@ import GraphQL +import Vapor /// Configuration options for GraphQLVapor public struct GraphQLConfig< @@ -27,7 +28,7 @@ public struct GraphQLConfig< subscriptionProtocols: Set = [], websocket: WebSocket = .init( // Including this strongly-typed argument is required to avoid compiler failures on Swift 6.2.3. - onWebSocketInit: { (_: EmptyWebSocketInit) in } + onWebSocketInit: { (_: EmptyWebSocketInit, _: Request) in } ), additionalValidationRules: [@Sendable (ValidationContext) -> Visitor] = [] ) { @@ -70,14 +71,14 @@ public struct GraphQLConfig< } public struct WebSocket: Sendable { - let onWebSocketInit: @Sendable (WebSocketInit) async throws -> WebSocketInitResult + let onWebSocketInit: @Sendable (WebSocketInit, Request) async throws -> WebSocketInitResult /// GraphQL over WebSocket configuration /// - Parameter onWebSocketInit: A custom callback run during `connection_init` resolution that allows /// authorization using the `payload` field of the `connection_init` message. /// Throw from this closure to indicate that authorization has failed. public init( - onWebSocketInit: @Sendable @escaping (WebSocketInit) async throws -> WebSocketInitResult = { (_: EmptyWebSocketInit) in } + onWebSocketInit: @Sendable @escaping (WebSocketInit, Request) async throws -> WebSocketInitResult = { (_: EmptyWebSocketInit, _: Request) in } ) { self.onWebSocketInit = onWebSocketInit } diff --git a/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift b/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift index 216ba33..edbdcff 100644 --- a/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift +++ b/Sources/GraphQLVapor/WebSocket/GraphQLHandler+handleWebSocket.swift @@ -39,7 +39,7 @@ extension GraphQLHandler { let server = GraphQLTransportWS.Server>( messenger: messenger, onInit: { initPayload in - try await config.websocket.onWebSocketInit(initPayload) + try await config.websocket.onWebSocketInit(initPayload, request) }, onExecute: { graphQLRequest, initResult in let graphQLContextComputationInputs = GraphQLContextComputationInputs( @@ -83,7 +83,7 @@ extension GraphQLHandler { let server = GraphQLWS.Server>( messenger: messenger, onInit: { initPayload in - try await config.websocket.onWebSocketInit(initPayload) + try await config.websocket.onWebSocketInit(initPayload, request) }, onExecute: { graphQLRequest, initResult in let graphQLContextComputationInputs = GraphQLContextComputationInputs( From e3a1a1366c1f0fa7cff47cd6350ccd33ef759084 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 13 Feb 2026 12:01:43 -0700 Subject: [PATCH 6/6] test: Adds subscription init failure test --- Tests/GraphQLVaporTests/WebSocketTests.swift | 101 +++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/Tests/GraphQLVaporTests/WebSocketTests.swift b/Tests/GraphQLVaporTests/WebSocketTests.swift index 39c94e2..50fc9da 100644 --- a/Tests/GraphQLVaporTests/WebSocketTests.swift +++ b/Tests/GraphQLVaporTests/WebSocketTests.swift @@ -88,6 +88,107 @@ struct WebSocketTests { } } + @Test func subscriptionWithInitPayloadError() async throws { + try await withApp { app in + let pubsub = SimplePubSub() + let schema = try GraphQLSchema( + subscription: GraphQLObjectType( + name: "Subscription", + fields: [ + "hello": GraphQLField( + type: GraphQLString, + resolve: { source, _, _, _ in + source as! String + }, + subscribe: { _, _, _, _ in + await pubsub.subscribe() + } + ), + ] + ) + ) + + struct InitPayload: Equatable, Codable, Sendable { + let code: String + } + let acceptedCode = "abc" + + app.graphql( + schema: schema, + config: .init( + subscriptionProtocols: [.websocket], + websocket: .init( + onWebSocketInit: { (initPayload: InitPayload, _) in + initPayload.code == acceptedCode + } + ) + ) + ) { inputs in + // If the codes don't match, this will fail on the subscribe/execute request + guard let codeIsValid = inputs.websocketInitResult, codeIsValid else { + throw GraphQLError(message: "Unauthorized") + } + return EmptyContext() + } + + app.http.server.configuration.port = 0 + app.environment.arguments = ["serve"] + try await app.startup() + let port = try #require(app.http.server.shared.localAddress?.port) + try await WebSocket.connect( + to: "ws://localhost:\(port)/graphql", + headers: ["Connection": "Upgrade"], + on: MultiThreadedEventLoopGroup(numberOfThreads: 1) + ) { websocket in + let decoder = JSONDecoder() + websocket.onText { websocket, message in + do { + #expect(!message.starts(with: "44")) + let response = try #require(message.data(using: .utf8)) + if let _ = try? decoder.decode(GraphQLTransportWS.ConnectionAckResponse.self, from: response) { + try await websocket.send(#""" + { + "type": "subscribe", + "payload": { + "query": "subscription { hello }" + }, + "id": "1" + } + """#) + // Must wait for a few milliseconds for the subscription to get set up. + try await Task.sleep(for: .milliseconds(10)) + await pubsub.emit(event: "World") + } else if let _ = try? decoder.decode(GraphQLTransportWS.NextResponse.self, from: response) { + Issue.record("Expected Error: \(message)") + await pubsub.cancel() + } else if let _ = try? decoder.decode(GraphQLTransportWS.CompleteResponse.self, from: response) { + Issue.record("Expected Error: \(message)") + try await websocket.close() + } else if let errorResult = try? decoder.decode(GraphQLTransportWS.ErrorResponse.self, from: response) { + #expect(errorResult.payload[0].message == "Unauthorized") + await pubsub.cancel() + try await websocket.close() + } else { + Issue.record("Unrecognized message: \(message)") + return + } + } catch { + Issue.record("WebSocket error: \(error)") + return + } + } + do { + // Send incorrect code, expect an "Unauthorized" error on the subscribe call + try await websocket.send(#"{"type": "connection_init", "payload": {"code": "def"}}"#) + try await websocket.onClose.get() + } catch { + Issue.record("WebSocket error: \(error)") + return + } + } + } + } + @Test func subscription_GraphQLWS() async throws { try await withApp { app in let pubsub = SimplePubSub()