Skip to content

feat(@nestjs/graphql): allow custom WebSocket server in subscriptions config - #4105

Open
wasaybuilds wants to merge 1 commit into
nestjs:masterfrom
wasaybuilds:feat/custom-ws-server
Open

feat(@nestjs/graphql): allow custom WebSocket server in subscriptions config#4105
wasaybuilds wants to merge 1 commit into
nestjs:masterfrom
wasaybuilds:feat/custom-ws-server

Conversation

@wasaybuilds

Copy link
Copy Markdown

Description

Following the v14 release, this PR adds support for providing a custom WebSocket server instance via the server option in the graphql-ws subscriptions configuration.

When provided, @nestjs/graphql will use the given WebSocketServer instance instead of creating the default one.

Closes #2653

Motivation

Allows users to pass their own pre-configured WebSocket server instance (e.g. for custom connection handlers, compression settings, or alternative server setups) to graphql-ws.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation (types documented with JSDoc).
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@kamilmysliwiec

Copy link
Copy Markdown
Member

Opus:

Nice feature. One blocking issue, two smaller ones:

Blocking - custom server's path is never honored. In initialize(), path resolves to this.options.path (driver default, e.g. /graphql) before ever falling back to wss.options.path, since the driver always sets a path. So new WebSocketServer({ noServer: true, path: '/subscriptions' }) breaks both ways: /subscriptions fails the outer startsWith('/graphql') check, and /graphql passes the outer check but then gets 400'd by ws's own exact-match shouldHandle(). The custom server's own path needs to win when set.

!path || is a catch-all fallback. If no path resolves at all, this handles every upgrade on the http server, not just graphql-ws ones. Should fail closed instead.

No check that the custom server used noServer: true. Otherwise it'll double-handle the upgrade. Worth a doc note or a guard.

Also: the new tests fully mock ws, so they never exercise real handleUpgrade/shouldHandle — which is exactly what would've caught the path bug.

… config

Allow users to provide a custom WebSocket server instance via the 'server' option in graphql-ws configuration.

Closes nestjs#2653
@wasaybuilds
wasaybuilds force-pushed the feat/custom-ws-server branch from ba5d3cf to 2a1320b Compare August 18, 2026 09:47
@wasaybuilds

Copy link
Copy Markdown
Author

Opus:

Nice feature. One blocking issue, two smaller ones:

Blocking - custom server's path is never honored. In initialize(), path resolves to this.options.path (driver default, e.g. /graphql) before ever falling back to wss.options.path, since the driver always sets a path. So new WebSocketServer({ noServer: true, path: '/subscriptions' }) breaks both ways: /subscriptions fails the outer startsWith('/graphql') check, and /graphql passes the outer check but then gets 400'd by ws's own exact-match shouldHandle(). The custom server's own path needs to win when set.

!path || is a catch-all fallback. If no path resolves at all, this handles every upgrade on the http server, not just graphql-ws ones. Should fail closed instead.

No check that the custom server used noServer: true. Otherwise it'll double-handle the upgrade. Worth a doc note or a guard.

Also: the new tests fully mock ws, so they never exercise real handleUpgrade/shouldHandle — which is exactly what would've caught the path bug.

Thanks for the thorough review @kamilmysliwiec! All points have been addressed:

  1. Path Precedence: Updated resolution order so wss.options.path takes precedence over the driver's default fallback (wss?.options?.path ?? subConfig?.path ?? this.options.path), ensuring custom server paths like /subscriptions are honored.
  2. Fail Closed: Removed the !path || fallback — it now strictly checks if (path && req.url?.startsWith(path)).
  3. noServer: true Guard & Docs: Added a constructor validation check that throws if server.options?.noServer === false, alongside JSDoc documentation on the server property.
  4. Real ws Tests: Rewrote the test suite to use real WebSocketServer instances from ws rather than mocks, covering path precedence, noServer validation, and protocol filtering.

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.

Select websocket implementation in graphql-ws configuration

2 participants