Skip to content

fix(dev): do not leak a rejection when a proxied upgrade fails - #4494

Open
kedrzu wants to merge 1 commit into
nitrojs:v2from
kedrzu:fix/dev-unhandled-upgrade-rejection
Open

fix(dev): do not leak a rejection when a proxied upgrade fails#4494
kedrzu wants to merge 1 commit into
nitrojs:v2from
kedrzu:fix/dev-unhandled-upgrade-rejection

Conversation

@kedrzu

@kedrzu kedrzu commented Jul 30, 2026

Copy link
Copy Markdown

🔗 Linked issue

Resolves #4493

❓ Type of change

  • 📖 Documentation (updates to the documentation, readme, or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

DevServer.handleUpgrade is async, and both of its callers drop the promise it returns:

upgrade: (req, socket, head) => devServer.handleUpgrade(req, socket, head),
// ...
listener.server.on("upgrade", (req, sock, head) =>
  this.handleUpgrade(req, sock, head)
);

It rejects when no worker is available, and — far more often — from NodeDevWorker.handleUpgrade, which returns proxy.ws(...) without awaiting or catching it. Replacing the dev worker kills every websocket proxied to it, so that promise rejects with ECONNRESET and nothing catches it.

For Nitro alone that is a stray unhandled rejection. Under Nuxt it ends the session: the CLI treats any unhandled rejection as fatal and restarts the dev server, and with a client that keeps reconnecting it loops until the dev server is unusable. It has surfaced there repeatedly — 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.

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 with them.

The change. handleUpgrade now never rejects. There is nothing left to reply to the client with once a handshake is in flight, so a failed upgrade closes the socket, and only errors that are not a peer disconnect are reported. Fixing it here rather than in the listen listener is deliberate: Nuxt calls the exported upgrade method directly, so a guard on the listener alone would miss the case that motivated this.

DevWorker["handleUpgrade"] is widened to Promise<void> | void, which is what NodeDevWorker already returned, so awaiting it is type-correct.

main has the same shape in src/dev/server.tsupgrade() is async and server.on("upgrade", ...) drops the promise. Happy to follow up there if you would like it handled the same way.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

`DevServer.handleUpgrade` is async and both callers drop the promise it
returns: the `upgrade` method handed out by `createDevServer`, and the
`server.on("upgrade")` listener in `listen`. It rejects when no worker is
available, and — far more often — from `NodeDevWorker.handleUpgrade`,
which returns `proxy.ws(...)` unawaited. Replacing the dev worker kills
every websocket proxied to it, so that promise rejects with `ECONNRESET`
and nothing is there to catch it.

For Nitro alone that is a stray unhandled rejection. Under Nuxt it ends
the session, because the CLI treats any unhandled rejection as fatal and
restarts the dev server; with a client that reconnects it loops.

Make `handleUpgrade` never reject. There is nothing left to reply with
once a handshake is in flight, so a failed upgrade closes the socket, and
only errors that are not a peer disconnect are reported.

Resolves nitrojs#4493

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kedrzu
kedrzu requested a review from pi0 as a code owner July 30, 2026 11:29
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

@kedrzu is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bb35838b-4111-42db-8ebc-8a280451ec1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant