Skip to content

feat: add the GraphQL WebSocket server (G4, graphql-transport-ws) - #1593

Open
jlouvel wants to merge 3 commits into
feat/graphql-g3-ssefrom
feat/graphql-g4-websocket-server
Open

jlouvel wants to merge 3 commits into
feat/graphql-g3-ssefrom
feat/graphql-g4-websocket-server

Conversation

@jlouvel

@jlouvel jlouvel commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

Part of #1586 (server side) and #1581.

Stacked on #1592 (G3) → #1591 → #1590 → #1589. The client side of G4 follows in a separate PR, stacked on this one and on #1568 (#1567).

Summary

GraphqlServerAdapter serves graphql-transport-ws when GraphqlTransport.WEBSOCKET is enabled:

graphql.getSubscriptionTransports().add(GraphqlTransport.WEBSOCKET);
router.attach("/graphql", graphql);   // HTTP, SSE and WebSocket on one path
  • WebSocket upgrades offering graphql-transport-ws are accepted on the adapter's own path, after the same filters and authenticators as HTTP requests. Other subprotocols get 400. HTTP/1.1 only, as for the WebSocket extension.
  • Protocol: connection_init / connection_ack within connectionInitTimeout (3 s, else close 4408), ping / pong, several operations per connection (each on its own virtual thread), complete in both directions, error for errors before execution (no complete after it), and close codes 4400 (invalid or binary message), 4401 (subscribe before the ack), 4409 (duplicate active id) and 4429 (repeated connection_init).
  • The connection_init payload reaches data fetchers under RestletGraphql.CONNECTION_INIT_PAYLOAD, next to REQUEST (the upgrade request) and CLIENT_INFO.
  • Subscriptions share the SSE limits and back-pressure: one result requested at a time, 64 active subscriptions per adapter (across SSE and WebSocket), 1 MiB of queued bytes. The SSE queueing subscriber is extracted as an internal GraphqlResultQueue and reused.
  • Closing a connection cancels its subscriptions; stopping the adapter closes connections with 1001.

Optional dependency. org.restlet.ext.websocket is requires static and an optional Maven dependency. The adapter only refers to the WebSocket code through a package-private interface, instantiated in start() when WEBSOCKET is enabled. GraphqlModulePathTestCase now resolves and runs its consumer without the WebSocket module.

Validation

./mvnw -B verify -P default,test -pl org.restlet.ext.graphql: 56 tests, 0 failures; 92.5% instruction coverage (gate 80%); Spotless, SpotBugs, Javadoc, -Werror and dependency convergence pass.

New GraphqlWebSocketServerTestCase (9 tests), driven by the JDK java.net.http.WebSocket client as an independent peer: handshake, ping/pong and the init payload in a data fetcher; subscription nexts and complete; error before execution; two subscriptions on one connection, client complete cancelling one, id reuse, connection close cancelling the rest; every close code (4400 for six kinds of invalid messages and binary frames, 4401, 4408, 4409, 4429); subprotocol required (400); HTTP on the same path; adapter stop closes with 1001; configuration validation.

The pom gets the same m2e-test-modules profile as the WebSocket extension, for the JDK HTTP client and server used by tests in Eclipse and VS Code.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical unresolved issues include operation ID reuse races, connections surviving shutdown, unbounded workers, and cancelled mutations still executing.

9 open issues
What changed in this PR

Adds the server-side G4 WebSocket transport to the GraphQL extension, allowing graphql-transport-ws alongside HTTP and SSE on the same endpoint. Client-side WebSocket support remains a separate follow-up.

Changes:

  • Adds connection initialization, multiplexed operations, cancellation, protocol errors, and shutdown handling.
  • Reuses the SSE result queue for subscription back-pressure and limits.
  • Keeps WebSocket dependencies optional and adds protocol and module-path tests.
File Description
org.restlet.ext.graphql/​src/​test/​java/​org/​restlet/​ext/​graphql/​GraphqlWebSocketServerTestCase.java Adds WebSocket protocol and lifecycle tests.
org.restlet.ext.graphql/​src/​test/​java/​org/​restlet/​ext/​graphql/​GraphqlTestSchemas.java Adds a fetcher exposing the initialization payload.
org.restlet.ext.graphql/​src/​test/​java/​org/​restlet/​ext/​graphql/​GraphqlModulePathTestCase.java Tests operation without the optional WebSocket module.
org.restlet.ext.graphql/​src/​main/​java/​org/​restlet/​ext/​graphql/​RestletGraphql.java Defines the initialization-payload context key.
org.restlet.ext.graphql/​src/​main/​java/​org/​restlet/​ext/​graphql/​internal/​GraphqlResultQueue.java Extracts the shared bounded subscription queue.
org.restlet.ext.graphql/​src/​main/​java/​org/​restlet/​ext/​graphql/​internal/​GraphqlEventStream.java Adopts the shared queue for SSE.
org.restlet.ext.graphql/​src/​main/​java/​org/​restlet/​ext/​graphql/​GraphqlWebSocketSupport.java Implements WebSocket connections and GraphQL operations.
org.restlet.ext.graphql/​src/​main/​java/​org/​restlet/​ext/​graphql/​GraphqlUpgradeSupport.java Isolates optional upgrade handling behind an interface.
org.restlet.ext.graphql/​src/​main/​java/​org/​restlet/​ext/​graphql/​GraphqlTransport.java Adds the WebSocket transport option.
org.restlet.ext.graphql/​src/​main/​java/​org/​restlet/​ext/​graphql/​GraphqlServerAdapter.java Integrates upgrades, timeout configuration, and execution context.
org.restlet.ext.graphql/​src/​main/​java/​module-info.java Declares the optional WebSocket module dependency.
org.restlet.ext.graphql/​pom.xml Adds the optional dependency and IDE test-module configuration.
changes.md Records server-side WebSocket support.

Review effort: Balanced

if (operations.putIfAbsent(id, operation) != null) {
return close(SUBSCRIBER_EXISTS, "Subscriber for " + id + " already exists");
}
Thread.ofVirtual().name("restlet-graphql-ws").start(operation);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this open for a decision: bounding in-flight operations per connection needs a new setting (or reusing an existing one with different semantics), which is an API choice for the G4 design rather than a review fix. Subscriptions are now reserved before execution, so maxSubscriptions does bound them; slow queries and mutations are not bounded yet.

jlouvel and others added 3 commits October 31, 2026 02:14
- GraphqlTransport.WEBSOCKET: the server adapter accepts WebSocket
  upgrades offering the graphql-transport-ws subprotocol on its own
  path, after the same filters and authenticators; other subprotocols
  get 400. HTTP keeps working on the same path.
- Protocol: connection_init and connection_ack with a configurable
  timeout (3 s, close 4408), ping and pong, several operations per
  connection on their own virtual threads, complete in both directions,
  error messages for errors before execution, and close codes 4400
  (invalid message), 4401 (subscribe before ack), 4409 (duplicate id)
  and 4429 (repeated connection_init).
- The connection_init payload reaches data fetchers under
  RestletGraphql.CONNECTION_INIT_PAYLOAD.
- Subscriptions share the SSE limits and back-pressure: the queueing
  subscriber of the SSE stream is extracted as GraphqlResultQueue.
  Closing a connection cancels its subscriptions; stopping the adapter
  closes connections with 1001.
- org.restlet.ext.websocket is optional (requires static, optional
  Maven dependency) and only loaded when WEBSOCKET is enabled. The
  named-module test now runs without it.

Refs #1586

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
A 300 ms connection_init timeout closed the connection before a slow CI
runner sent connection_init.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
Close connections accepted while stopping and close them concurrently,
reserve a subscription slot before executing a subscription, skip
operations cancelled before they start, and retire operation IDs
atomically with their messages. Validate ping and pong payloads, bound
close reasons to 123 bytes and let connection_init win or lose against
the timeout atomically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jlouvel
jlouvel force-pushed the feat/graphql-g4-websocket-server branch from ab5539d to 63ed523 Compare October 31, 2026 06:18
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants