Skip to content

feat: support custom request headers for WsProvider - #752

Open
sinzii wants to merge 2 commits into
mainfrom
tvu/ws-provider-request-headers
Open

feat: support custom request headers for WsProvider#752
sinzii wants to merge 2 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.

claude added 2 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
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