fix(express): skip the fallback 404 once the response has started - #2024
Open
dchaudhari7177 wants to merge 1 commit into
Open
fix(express): skip the fallback 404 once the response has started#2024dchaudhari7177 wants to merge 1 commit into
dchaudhari7177 wants to merge 1 commit into
Conversation
The SPA fallback answers every error from res.sendFile with a 404. That callback also reports a client abort, and by then the headers and part of the body are already on the wire, so res.status().send() throws ERR_HTTP_HEADERS_SENT. Express invokes the callback from a setImmediate, so the throw lands outside the middleware chain and outside any exception filter. An aborted page load therefore takes down the whole process instead of failing the one request. Bail out once res.headersSent is true, since there is no longer a response to write. Errors raised before anything is sent, such as a missing index file, still produce the 404. Closes nestjs#2023
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Checklist
PR Type
What is the current behavior?
Issue Number: #2023
The SPA fallback answers every
res.sendFileerror with a 404:sendFilealso reports a client abort through that callback, and by then the headers and part of the body are already on the wire.res.status().send()then throwsERR_HTTP_HEADERS_SENT. Because Express invokes the callback from asetImmediate, the throw lands outside the middleware chain and outside any exception filter — so a page reload or a flaky mobile connection during the index stream takes down the whole process, not just the request.What is the new behavior?
The 404 is skipped once
res.headersSentis true, since there is no longer a response to write. Errors raised before anything is sent — a missing or unreadable index file — still produce the 404 exactly as before.I confirmed the mechanism in isolation:
Tests
Added an e2e case that drives the real fallback callback with the response already started, standing in for the abort so the test is deterministic (a genuine mid-stream disconnect is a race). It captures anything the callback throws and asserts nothing was thrown.
Two things I had to get right for the test to be meaningful, both verified rather than assumed:
/some/spa/route). My first attempt used/, which express.static serves straight from disk —sendFilewas never called, so the test passed with and without the fix. I instrumented it to check, andsendFile called = falseconfirmed the test was vacuous.200either way, so status alone cannot distinguish the two.With both corrected, reverting only
lib/fails it precisely:Verification
npm run test:e2e→ 23 passed (22 before, +1 new)npm run build→ cleanoxlint lib/→ cleanI left the fastify loader alone: its 404 paths run before streaming begins (inside the
fs.statcallback, ahead ofcreateReadStream), so the headers are not yet sent there.prettier --checkflags both touched files, but that is pre-existing on a clean tree — verified by stashing — so I did not mix a reformat into this diff. Thelint-stagedhook did format the staged changes themselves.Does this PR introduce a breaking change?