Bug Description
WebSocketStream can throw TypeError [ERR_INVALID_STATE]: Invalid state: Controller is already closed when the readable side is canceled and the connection later completes a clean close handshake.
The failure happens because readable.cancel() closes the internal ReadableStreamDefaultController, and the clean socket close path later calls close() on the same controller again.
Reproducible By
- Start a WebSocket server.
- Connect with
new WebSocketStream(url).
- Wait for
wss.opened and get readable.
- Call
await readable.cancel(new Error('client cancel')).
- Trigger a clean close, for example with
wss.close() on the client and ws.close(1000, 'bye') on the server.
- Wait for the socket to close.
Minimal reproduction:
const { WebSocketServer } = require('ws')
const { WebSocketStream } = require('undici')
const server = new WebSocketServer({ port: 3000 })
server.on('connection', (ws) => {
setTimeout(() => {
ws.send('hello')
setTimeout(() => ws.close(1000, 'bye'), 20)
}, 200)
})
;(async () => {
const wss = new WebSocketStream('ws://127.0.0.1:3000')
const { readable } = await wss.opened
await readable.cancel(new Error('client cancel')).catch(() => {})
wss.close()
await wss.closed.catch(() => {})
})()
Expected Behavior
Canceling the readable side before a later clean close should not throw or surface an uncaught exception. The connection should close normally and settle closed without crashing the process.
Logs & Screenshots
TypeError [ERR_INVALID_STATE]: Invalid state: Controller is already closed
at ReadableStreamDefaultController.close (node:internal/webstreams/readablestream:1068:13)
at #onSocketClose (.../lib/web/websocket/stream/websocketstream.js:389:38)
Environment
Linux
Node.js v22.x
undici main branch
Additional context
The clean close path in lib/web/websocket/stream/websocketstream.js currently calls this.#readableStreamController.close() directly. That is not safe after ReadableStream.cancel() because the controller is already closed at that point.
Bug Description
WebSocketStreamcan throwTypeError [ERR_INVALID_STATE]: Invalid state: Controller is already closedwhen the readable side is canceled and the connection later completes a clean close handshake.The failure happens because
readable.cancel()closes the internalReadableStreamDefaultController, and the clean socket close path later callsclose()on the same controller again.Reproducible By
new WebSocketStream(url).wss.openedand getreadable.await readable.cancel(new Error('client cancel')).wss.close()on the client andws.close(1000, 'bye')on the server.Minimal reproduction:
Expected Behavior
Canceling the readable side before a later clean close should not throw or surface an uncaught exception. The connection should close normally and settle
closedwithout crashing the process.Logs & Screenshots
TypeError [ERR_INVALID_STATE]: Invalid state: Controller is already closed at ReadableStreamDefaultController.close (node:internal/webstreams/readablestream:1068:13) at #onSocketClose (.../lib/web/websocket/stream/websocketstream.js:389:38)Environment
Linux
Node.js v22.x
undici main branch
Additional context
The clean close path in
lib/web/websocket/stream/websocketstream.jscurrently callsthis.#readableStreamController.close()directly. That is not safe afterReadableStream.cancel()because the controller is already closed at that point.