Skip to content

feat: support custom request headers for WsProvider - #752

Merged
sinzii merged 3 commits into
mainfrom
tvu/ws-provider-request-headers
Aug 30, 2026
Merged

feat: support custom request headers for WsProvider#752
sinzii merged 3 commits into
mainfrom
tvu/ws-provider-request-headers

Conversation

@sinzii

@sinzii sinzii commented Aug 17, 2026

Copy link
Copy Markdown
Member

Closes #751

Adds a headers option to WsProviderOptions so custom HTTP headers (e.g: an auth token for a private RPC endpoint or proxy) can be sent along with the websocket opening handshake.

const provider = new WsProvider({
  endpoint: 'wss://private.rpc',
  headers: {
    Authorization: `Bearer ${process.env.API_TOKEN}`,
    'X-Client-ID': 'example-client',
  },
});

Headers can also be resolved on each connection attempt (including reconnects), which is helpful to refresh short-lived tokens or to use different credentials per endpoint. The selector receives the same WsConnectionState as the endpoint selector, so the endpoint about to be connected to is available:

const provider = new WsProvider({
  endpoint: ['wss://private.rpc', 'wss://private-backup.rpc'],
  headers: async ({ attempt, currentEndpoint }) => ({
    Authorization: `Bearer ${await fetchToken(currentEndpoint)}`,
  }),
});

No dependency changes — the websocket is still created through @polkadot/x-ws as before, and yarn.lock is untouched.

Why the headers are passed as the second constructor argument

@polkadot/x-ws resolves its WebSocket export via extractGlobal('WebSocket', ws), so the underlying implementation depends on the runtime:

  • Node.js < 22 → the ws package
  • Node.js >= 22 / Bun → the native (undici) WebSocket, since a global WebSocket is defined

Passing the options as a third argument (new WebSocket(url, undefined, { headers })) only works for the ws package and silently drops the headers on Node.js >= 22. Both implementations do accept an options object as the second constructor argument, which is what this PR uses — verified against both implementations, the header reaches the server and no bogus Sec-WebSocket-Protocol is sent.

Browsers and Deno follow the WHATWG spec where the second argument is a list of subprotocols, so headers cannot be sent there. In those environments the headers are ignored and a warning is logged once per provider instance (the headers selector is not invoked at all, to avoid a pointless token fetch). This is checked by a new canSendRequestHeaders() helper. Headers are never logged.

When no headers are configured, the websocket is constructed exactly as before.

Tests

  • WsProvider.spec.ts: the @polkadot/x-ws mock now records constructor arguments. New cases cover: no options passed when no headers are configured (guards the existing behavior), static headers, the selector receiving the connection state, headers being re-resolved on reconnection, an empty headers map being ignored, and headers being dropped with a warning when the environment does not support them.
  • WsProviderHeaders.spec.ts (new): runs against a real websocket handshake without mocking the websocket implementation (// @vitest-environment node), asserting the headers actually arrive over the wire. This is what verifies the argument shape against whichever implementation the runtime picks — CI runs Node 18 (ws), locally verified on Node 22 (native). The server is a minimal handshake-only node:http server, so no extra dependency is needed (@polkadot/x-ws only ships a client). Cross-checked by mutation: switching the implementation to the third-argument form makes this test fail on Node 22, which is exactly the regression it guards.
  • utils.spec.ts: cases for canSendRequestHeaders on Node.js, Bun, Deno and browsers.

Docs are covered by the WsProvider JSDoc examples and a section in packages/providers/README.md; the user-facing docs site would need a matching follow-up.


Note

Medium Risk
Changes how WebSockets are opened and may carry auth secrets, but behavior is unchanged when headers is omitted and unsupported environments fail safe with warnings.

Overview
Adds a headers option on WsProvider so callers can attach custom HTTP headers to the WebSocket opening handshake (e.g. auth for private RPCs). Values can be a static map or a selector invoked on each connect/reconnect with the same WsConnectionState as endpoint selection, for token refresh or per-endpoint credentials.

Connection logic resolves headers via new #getHeaders(), uses canSendRequestHeaders() to enable this only on Node.js/Bun, and passes { headers } as the second WebSocket constructor argument so headers work with both ws and native Node WebSocket (avoids silent drops on newer Node). Unsupported runtimes ignore headers, log a one-time warning, and skip the selector so token fetches are not wasted.

README and JSDoc document usage; tests cover constructor args (mocked), on-the-wire handshake (Node integration), runtime detection, reconnect re-resolution, and empty-header behavior.

Reviewed by Cursor Bugbot for commit 0d1f55d. Bugbot is set up for automated code reviews on this repo. Configure here.

claude added 3 commits August 16, 2026 07:00
Add a `headers` option to `WsProviderOptions` so custom HTTP headers
(e.g: an auth token for a private RPC endpoint or proxy) can be sent
along with the websocket opening handshake.

Headers can be provided either as a static map or as a function that is
resolved on every connection attempt (including reconnects), which is
helpful to refresh short-lived tokens or to use different credentials
per endpoint.

The headers are passed as an options object in the second constructor
argument, which is supported by both the `ws` package (used by
`@polkadot/x-ws` on Node.js < 22) and the native WebSocket
implementation (Node.js >= 22, Bun). Browsers and Deno follow the WHATWG
spec where the second argument is a list of subprotocols, so headers are
ignored there with a warning.

Closes #751

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cpx2P4VETQHZ3hXGphLUAy
`@polkadot/x-ws` only ships a websocket client, so the previous test
relied on the `ws` package for a server. Use a minimal handshake-only
server built on `node:http` instead, keeping the test dependency-free
while still exercising the real websocket implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cpx2P4VETQHZ3hXGphLUAy
Verified across Node.js v18.20.8, v20.10.0, v20.20.2, v21.0.0, v21.7.3,
v22.0.0, v22.23.2, v24.20.0, v26.8.1, Bun 1.4.0 and Deno 1.46/2.0/2.9:

- A global WebSocket is only defined by default from Node.js v22 onwards
  (v20.10+ define one when started with `--experimental-websocket`), so
  the boundary between the `ws` package and the native implementation is
  not strictly the major version.
- Passing the options as the second constructor argument sends the
  headers on every tested Node.js version and on Bun, while the third
  argument only works for the `ws` package.
- Deno shims `process.versions.node`, but only very recent versions
  accept the options object, older ones throw `Invalid protocol value`,
  hence keeping it out of the supported runtimes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cpx2P4VETQHZ3hXGphLUAy
@sinzii

sinzii commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@cursor

cursor Bot commented Aug 29, 2026

Copy link
Copy Markdown

Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings.

@sinzii

sinzii commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0d1f55d. Configure here.

@sinzii
sinzii merged commit 8b52df5 into main Aug 30, 2026
5 checks passed
@sinzii
sinzii deleted the tvu/ws-provider-request-headers branch August 30, 2026 05:24
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.

No support for request headers for WsProvider

2 participants