fix(dev): do not restart when a client aborts a connection by kedrzu · Pull Request #1412 · nuxt/cli · GitHub
Skip to content

fix(dev): do not restart when a client aborts a connection - #1412

Merged
danielroe merged 3 commits into
nuxt:mainfrom
kedrzu:fix/dev-ignore-aborted-connections
Jul 31, 2026
Merged

danielroe merged 3 commits into
nuxt:mainfrom
kedrzu:fix/dev-ignore-aborted-connections

Conversation

@kedrzu

@kedrzu kedrzu commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

nuxt dev can restart itself in a loop with:

 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

A client hanging up mid-request is ordinary dev-server traffic — a browser reloading while a page is still streaming, a tab closed mid-navigation, a stale client from another project reconnecting to a port this server now owns. When such an error surfaces as an unhandled rejection, both places that watch for one treat it as fatal:

  • createRestartHook schedules a hard restart;
  • the forked dev process reports it over IPC and calls process.exit().

With a client that keeps reconnecting, this loops until the dev server is unusable — every reconnect kills the session that was just rebuilt.

Where the rejection comes from

I traced the origin in a real project by wrapping the candidate call sites and logging which one rejects. It is NodeDevWorker.handleUpgrade in nitro, which returns proxy.ws(...) without awaiting or catching it:

// nitropack/dist/core/index.mjs
handleUpgrade(req, socket, head) {
  if (!this.ready) { return }
  return this.#proxy.proxy.ws(req, socket, { target: this.#address, xfwd: true }, head)
}

When the Nitro dev worker is replaced, every proxied websocket dies and that promise rejects. Adding a .catch() there removed the rejections entirely (8 in 30s → 0) and the restarts with them, so the leak itself belongs in nitro and I am happy to open that separately.

This PR is the guard on the other side: whatever leaks it, a connection the peer dropped should not end the dev session.

Change

isBrokenPipe already encodes exactly this rule — "a property of the other end of the pipe, not a fault in the dev server". This adds a sibling predicate for aborted connections (ECONNRESET, ECONNABORTED, ERR_STREAM_PREMATURE_CLOSE) and applies it in both fatal paths.

The IPC handler also moves from once to on, so an ignored connection error no longer consumes the listener and leaves a later genuine rejection unreported.

Logging is deliberately untouched: the error still reaches the reporter, so the event stays visible in the terminal — it just no longer ends the session.

Tests

  • errors.spec.ts — coverage for the new predicate.
  • restart-hook.spec.ts — an ECONNRESET no longer triggers the restart callback, while an unrelated error still does.

pnpm lint, pnpm test:types and pnpm test:knip are clean. pnpm test:unit passes except for two pre-existing failures in test/e2e/commands.spec.ts (dev, works with dev server, both expecting 200 and getting 404) — I confirmed those fail identically on an unmodified main in my environment, so they are unrelated to this change.

A client hanging up mid-request is normal dev-server traffic: a browser
reloading while a page is still streaming, a tab closed mid-navigation,
or a proxied websocket dropped when the Nitro dev worker is replaced.

Such an error can surface as an unhandled rejection, and both places that
watch for one treat it as fatal: `createRestartHook` schedules a hard
restart, and the forked dev process reports the rejection over IPC and
exits. The result is `Restarting Nuxt due to error: read ECONNRESET`, and
with a client that keeps reconnecting it can loop until the dev server is
unusable.

`isBrokenPipe` already encodes the rule that an error belonging to the
other end of a connection is not this process crashing. This extends that
to aborted connections and applies it in both fatal paths. The IPC handler
switches from `once` to `on`, so that an ignored connection error does not
consume the listener and leave a later genuine rejection unreported.

Logging is left as it is: the error still reaches the reporter, so the
event stays visible without ending the session.

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

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

@codecov-commenter

codecov-commenter commented Jul 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.58824% with 5 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@f6ec495). Learn more about missing BASE report.

Files with missing lines Patch % Lines
packages/nuxt-cli/src/dev/index.ts 75.00% 3 Missing ⚠️
packages/nuxt-cli/src/utils/console.ts 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1412   +/-   ##
=======================================
  Coverage        ?   62.17%           
=======================================
  Files           ?       96           
  Lines           ?     4772           
  Branches        ?     1463           
=======================================
  Hits            ?     2967           
  Misses          ?     1459           
  Partials        ?      346           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Codecov flagged the branch inside the IPC constructor as untested, and it
could not be reached: `IPC` is instantiated at import time and only arms
its listener inside a fork.

Lift the decision into `createRejectionHandler`, alongside the existing
`createRestartHook`, and take the report and stop side effects as
arguments. The constructor keeps only the wiring, and both branches —
report and exit, or ignore an aborted connection — are now unit-tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
  • nuxt-cli-playground

    npm i https://pkg.pr.new/create-nuxt@1412
    
    npm i https://pkg.pr.new/nuxi@1412
    
    npm i https://pkg.pr.new/@nuxt/cli@1412
    

commit: d90cc3a

@codspeed

codspeed Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing kedrzu:fix/dev-ignore-aborted-connections (d90cc3a) with main (f30cdf3)

Open in CodSpeed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nuxt-cli/test/unit/restart-hook.spec.ts`:
- Around line 132-142: Extend the restart-hook tests around
createRejectionHandler to cover the IPC listener lifecycle: enable IPC, register
the returned rejection handler on the process event listener, emit an aborted
rejection first, then a genuine rejection, and assert the genuine rejection is
reported and stops the child. Ensure the test would fail if the listener were
registered with process.once instead of process.on.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 14126d75-4cb3-47a2-81cb-948b0d7c8503

📥 Commits

Reviewing files that changed from the base of the PR and between 17128ba and fa6fa33.

📒 Files selected for processing (2)
  • packages/nuxt-cli/src/dev/index.ts
  • packages/nuxt-cli/test/unit/restart-hook.spec.ts

Comment on lines +132 to +142
it('should keep the process alive when a client aborted the connection', () => {
const report = vi.fn()
const stop = vi.fn()
const handle = createRejectionHandler(report, stop)

handle(Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }))
handle(Object.assign(new Error('premature close'), { code: 'ERR_STREAM_PREMATURE_CLOSE' }))

expect(report).not.toHaveBeenCalled()
expect(stop).not.toHaveBeenCalled()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Cover the IPC listener lifecycle.

These tests call the returned callback directly, so they cannot detect a regression from process.on back to process.once. Add a test that enables IPC, emits an aborted rejection followed by a genuine one, and verifies the latter is reported/stops the child.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxt-cli/test/unit/restart-hook.spec.ts` around lines 132 - 142,
Extend the restart-hook tests around createRejectionHandler to cover the IPC
listener lifecycle: enable IPC, register the returned rejection handler on the
process event listener, emit an aborted rejection first, then a genuine
rejection, and assert the genuine rejection is reported and stops the child.
Ensure the test would fail if the listener were registered with process.once
instead of process.on.

@kedrzu

kedrzu commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

@danielroe danielroe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you ❤️

@danielroe
danielroe enabled auto-merge July 31, 2026 11:21
@danielroe
danielroe disabled auto-merge July 31, 2026 11:34
@danielroe
danielroe merged commit 810231e into nuxt:main Jul 31, 2026
15 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 31, 2026
This was referenced Aug 24, 2026
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.

3 participants