mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
[PR #915] [CLOSED] fix(dev): patch EventEmitter.emit to swallow peer-disconnect errors #945
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#945
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/cloudflare/vinext/pull/915
Author: @james-elicx
Created: 4/26/2026
Status: ❌ Closed
Base:
main← Head:claude/dev-socket-emit-patch📝 Commits (1)
7894815fix(dev): patch EventEmitter.emit to swallow peer-disconnect errors📊 Changes
1 file changed (+84 additions, -38 deletions)
View changed files
📝
packages/vinext/src/index.ts(+84 -38)📄 Description
Summary
Third pass at #905. Both #911 (per-connection socket listener) and #913 (process-level
uncaughtExceptionbackstop) left a hole — the dev server still crashes in this exact shape after a successful response, with both prior fixes installed:Two things in that trace defeat the previous fixes:
Socket.onerroratreadable.js:1035is Node'spipe()re-emission. When a piped source errors and the destination has no'error'listener,pipe()emits the error on the destination — and the destination here is not the inbound connection socket the per-connection guard attached to.Socket.emitatnode:domain:489is thenode:domainwrapper. Errors routed through a domain bypassuncaughtExceptionentirely, so the process-level handler from #913 never runs.Three real call sites in vinext dev hit this:
fromWeb(fetch().body).pipe(res)inproxyExternalRewriteNode@vitejs/plugin-rscwith their own pipe topologyfetch()Approach
The only place to catch every variant is at the synchronous source:
EventEmitter.prototype.emit. Wrap it once, short-circuit only when all three of these hold:type === 'error'ECONNRESET/EPIPE/ECONNABORTED)'error'listeners (the exact condition under which Node would throw)Anything else passes through untouched — including non-peer-disconnect errors, and peer-disconnect errors on emitters that already have a listener (so registered listeners still receive them).
Installed once per process via a
Symbol.forguard so dep re-optimization, full reloads, and repeated plugin invocations can't double-install or tear it down.The belt-and-braces
uncaughtException/unhandledRejectionhandlers from #913 stay in place for surfaces that bypassEventEmitter.emitentirely (raw promise rejections, native callbacks). Both still sync-throw on non-peer-disconnect errors to preserve Node's default crash semantics.Verification
Behavior matrix locally:
Dev-only by virtue of running inside the Vite plugin factory; not invoked in production (Cloudflare Worker).
Refs
Test plan
vp checkpasses (one pre-existing benchmarksCannot find module 'vinext'error is unrelated)proxyExternalRewriteNode/RSC streaming: dev server no longer crashes after browser disconnect mid-stream, including across dep re-optimization'error'listeners on streams still receive events normally🤖 Generated with Claude Code
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.