dev: unhandled ECONNRESET when a proxied websocket dies with its worker
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 11.2k
- Forks
- 899
- Avg merge
- 2d 24m
- Merged PRs (30d)
- 40
Description
Environment
- Nitro: 2.13.4
- Nuxt: 4.2.2,
@nuxt/cli3.35.1 (also reproduced against 3.36.0) - Node: v24 (macOS); the same code path is present on all platforms
- Preset:
nodedev worker (defaultnuxt dev)
Reproduction
Any nuxt dev session where a proxied websocket dies:
npx nuxi init repro && cd repro && npm i && npm run dev- Open
http://localhost:3000and leave the tab open. - Edit a file that ends up in the server bundle (e.g. a
server/api/*handler, or a module imported by one), so the dev worker is replaced. - Repeat, or keep any client that reconnects pointed at the port.
The dev server then prints and restarts itself:
ERROR [unhandledRejection] read ECONNRESET
at TCP.onStreamRead (node:internal/stream_base_commons:216:20)
at TCP.callbackTrampoline (node:internal/async_hooks:130:17)
● Restarting Nuxt due to error: Error: read ECONNRESET
I could not attach a StackBlitz because the failure needs a websocket to be alive across a worker swap, which the in-browser container does not really exercise — but the cause is a plain unhandled promise, visible in the source, and I verified it by instrumentation rather than by guessing (below).
Describe the bug
DevServer.handleUpgrade is async, and both of its callers drop the promise it returns:
// src/core/dev-server/server.ts
upgrade: (req, socket, head) => devServer.handleUpgrade(req, socket, head),
// ...
listener.server.on("upgrade", (req, sock, head) =>
this.handleUpgrade(req, sock, head)
);
It can reject in two ways: createError({ statusCode: 503 }) when no worker is available, and — far more often — from NodeDevWorker.handleUpgrade, which returns proxy.ws(...) without awaiting or catching it:
// src/core/dev-server/worker.ts
handleUpgrade(req: IncomingMessage, socket: Socket, head: any) {
if (!this.ready) {
return;
}
return this.#proxy!.proxy.ws(req, socket, { target: this.#address, xfwd: true }, head);
}
Every time the dev worker is replaced, the websockets proxied to it die and that promise rejects with ECONNRESET. Nothing catches it, so it becomes an unhandled rejection.
For Nitro alone that is a stray warning. Under Nuxt it ends the session: @nuxt/cli treats any unhandled rejection as fatal and restarts the dev server. With a client that keeps reconnecting it loops, and the dev server never stays up long enough to be useful. It has been reported against the CLI several times — nuxt/cli#247, nuxt/cli#968, nuxt/cli#994 — where it reads as a Windows/HMR flake, but the leak is here and platform-independent.
Additional context
I traced it rather than inferred it: I wrapped both candidate call sites in a real project and logged which one rejected. Over 30 seconds — NodeDevWorker.handleUpgrade 8, the CLI request handler 0. Adding a .catch() at that call site removed the rejections and the restarts entirely.
I am opening a PR against v2 that makes handleUpgrade never reject: it closes the socket on failure and only logs errors that are not a peer disconnect (there is nothing to reply to the client with once a handshake is in flight). main has the same shape in src/dev/server.ts (upgrade() is async and server.on("upgrade", ...) drops the promise), so it likely wants the same treatment — happy to follow up there if you would like.
Logs
ERROR [unhandledRejection] read ECONNRESET
at TCP.onStreamRead (node:internal/stream_base_commons:216:20)
at TCP.callbackTrampoline (node:internal/async_hooks:130:17)
│
● Restarting Nuxt due to error: Error: read ECONNRESET
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/core/dev-server/server.ts and src/core/dev-server/worker.ts, especially the upgrade handlers and NodeDevWorker.handleUpgrade. Run the reported nuxt dev reproduction while replacing a server-bundle file, then verify that worker websocket disconnects no longer produce an unhandled ECONNRESET or restart loop. Check the corresponding src/dev/server.ts path if the fix covers both branches.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- backend, devtools
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100