nitrojs / nitrojs/nitro

dev: unhandled ECONNRESET when a proxied websocket dies with its worker

Open
#4,493 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug nuxt v2
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/cli 3.35.1 (also reproduced against 3.36.0)
  • Node: v24 (macOS); the same code path is present on all platforms
  • Preset: node dev worker (default nuxt dev)
Reproduction

Any nuxt dev session where a proxied websocket dies:

  1. npx nuxi init repro && cd repro && npm i && npm run dev
  2. Open http://localhost:3000 and leave the tab open.
  3. 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.
  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.