Skip to content

fix: don't crash the CLI when the upstream dev server dies mid-response - #8386

Open
NMiller3862 wants to merge 1 commit into
netlify:mainfrom
NMiller3862:fix/proxy-error-headers-sent-guard
Open

fix: don't crash the CLI when the upstream dev server dies mid-response#8386
NMiller3862 wants to merge 1 commit into
netlify:mainfrom
NMiller3862:fix/proxy-error-headers-sent-guard

Conversation

@NMiller3862

Copy link
Copy Markdown

Summary

Fixes the crash class tracked in #5917 (also #7060, #5676): when the framework dev server that netlify dev proxies to dies mid-response, the proxy's error handler calls res.writeHead(500, …) on a response whose headers are already on the wire. ERR_HTTP_HEADERS_SENT is thrown out of an event handler, and the whole CLI terminates:

Error: Cannot write headers after they are sent to the client
    at ServerResponse.writeHead (node:_http_server:354:11)
    at ProxyServer.<anonymous> (src/utils/proxy.ts:559:11)
    at ClientRequest.proxyError (http-proxy/lib/http-proxy/passes/web-incoming.js:165:18)
⬥ Netlify CLI has terminated unexpectedly.

One condition fixes it: if res.headersSent || res.writableEnded, the response is unsalvageable — destroy the socket and return, instead of throwing. The one in-flight request was doomed either way; the CLI survives, and requests after the upstream recovers succeed.

Deterministic reproduction

This crash has historically been hard to pin ("sometimes when I save a file", #5917). Next.js provides a deterministic trigger: its dev server restarts itself when the V8 heap passes 80% of its limit (next/dist/server/lib/start-server.jsprocess.exit(RESTART_EXIT_CODE)), expecting its supervisor to bring it back — which it does, in ~3s. Any request mid-flight through netlify-cli's proxy at that moment hits this handler with headers already sent.

Repro: netlify dev wrapping a large Next 16 app, drive sustained browser traffic (we used a ~480-test Playwright suite) until Next logs Server is approaching the used memory threshold, restarting.... Unpatched: the CLI died on this line in 2 of 2 runs at that moment. Patched (same workload, same default heap): 2 restarts occurred, CLI survived both, zero ERR_CONNECTION_REFUSED in the suite — 457/486 tests passed vs. a cascade of 289 spurious failures when the CLI dies.

Notes

  • writableEnded is included alongside headersSent because res.end() after end also errors; ended implies headers sent for HTTP/1, so this is belt-and-braces.
  • The Socket (upgrade) path is unchanged.
  • Scoped deliberately to the error handler — the crash site with three linked issues — rather than guarding all writeHead call sites.

@NMiller3862
NMiller3862 requested a review from a team as a code owner August 3, 2026 08:25
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved proxy error handling to prevent server crashes when a response has already started or finished.
    • Development server now remains running instead of producing additional response errors.

Walkthrough

The proxy error handler now checks whether the response headers were sent or the writable stream ended. When either condition is true, it destroys the response socket and returns instead of sending a 500 response.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: serhalp

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes preventing CLI crashes when the proxied upstream development server dies mid-response.
Description check ✅ Passed The description accurately explains the crash, root cause, fix, reproduction, validation, and scope of the changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/utils/proxy.ts (2)

560-564: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the explanatory comments in this branch.

The condition, res.destroy(), and return already express the behavior. Remove the explanatory block, or retain only a short issue reference if project policy requires one.

As per coding guidelines: **/*.{ts,tsx} files must not contain comments describing what the code does.

🤖 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 `@src/utils/proxy.ts` around lines 560 - 564, Remove the multi-line explanatory
comment in the upstream-response failure branch of the proxy handler, leaving
the existing condition, res.destroy(), and return behavior unchanged. Retain
only a brief issue reference if required by project policy.

Source: Coding guidelines


559-567: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add regression coverage for the committed-response guard.

Cover an upstream error after headers are sent and after the response has ended. Assert that the handler destroys the response, returns before res.writeHead(500, ...), and does not raise ERR_HTTP_HEADERS_SENT. Also preserve coverage for the uncommitted-response 500 fallback.

🤖 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 `@src/utils/proxy.ts` around lines 559 - 567, Add regression tests for the
proxy error handler covering upstream failures after headers are sent and after
the response has ended: assert that res.destroy() is called, execution returns
before res.writeHead(500, ...) and no ERR_HTTP_HEADERS_SENT is raised. Retain
coverage asserting the existing 500 fallback for uncommitted responses.
🤖 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.

Nitpick comments:
In `@src/utils/proxy.ts`:
- Around line 560-564: Remove the multi-line explanatory comment in the
upstream-response failure branch of the proxy handler, leaving the existing
condition, res.destroy(), and return behavior unchanged. Retain only a brief
issue reference if required by project policy.
- Around line 559-567: Add regression tests for the proxy error handler covering
upstream failures after headers are sent and after the response has ended:
assert that res.destroy() is called, execution returns before res.writeHead(500,
...) and no ERR_HTTP_HEADERS_SENT is raised. Retain coverage asserting the
existing 500 fallback for uncommitted responses.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 60bed017-34f0-4045-a1f8-8aa75a95cc9f

📥 Commits

Reviewing files that changed from the base of the PR and between 6492300 and 5c6b198.

📒 Files selected for processing (1)
  • src/utils/proxy.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • netlify/blueprints (manual)

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