Skip to content

fix(core): defer sse writehead until after lifecycle completes#16753

Open
jkalberer wants to merge 1 commit intonestjs:masterfrom
jkalberer:fix/sse-pipe-validation-error-status
Open

fix(core): defer sse writehead until after lifecycle completes#16753
jkalberer wants to merge 1 commit intonestjs:masterfrom
jkalberer:fix/sse-pipe-validation-error-status

Conversation

@jkalberer
Copy link
Copy Markdown

@jkalberer jkalberer commented Apr 10, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

When using @Sse() endpoints with ValidationPipe, pipe errors (e.g. BadRequestException) were returned with HTTP 200 because SseStream.pipe() eagerly called writeHead(200) before the interceptor chain had finished subscribing and running pipes. Once headers were sent, exception filters could not override the status code.

Issue Number: N/A

What is the new behavior?

  • SseStream: writeHead is no longer called from pipe(); it runs only via commitHeaders() (also invoked from _transform() when data is written).
  • RouterResponseController.sse(): Subscribes with catchError so errors before headers are committed propagate to exception filters; after commit, errors become SSE error events. Uses pipe(..., { end: false }) and a setTimeout(0) call to commitHeaders() so validation/pipe failures (microtasks) skip sending headers, while successful routes open the stream quickly without waiting for the first emission.
  • RouterExecutionContext: Passes statusCode and getHeaders() from the response at handle time.
  • Tests: Unit tests for the above paths; Express/Fastify integration tests assert 400 + JSON for validation errors before the SSE stream opens.

Does this PR introduce a breaking change?

  • Yes
  • No

Previously, mutating response.statusCode or getHeaders() only inside the returned observable (after the handle ran) could affect which values were sent with SSE headers. Status and headers are now captured when the SSE response handler runs, consistent with the rest of the HTTP pipeline.

Other information

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 10, 2026

Coverage Report for CI Build 0

Coverage decreased (-0.03%) to 89.878%

Details

  • Coverage decreased (-0.03%) from the base build.
  • Patch coverage: 8 uncovered changes across 1 file (52 of 60 lines covered, 86.67%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
packages/core/router/router-response-controller.ts 39 31 79.49%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 8496
Covered Lines: 7636
Line Coverage: 89.88%
Relevant Branches: 2872
Covered Branches: 2327
Branch Coverage: 81.02%
Branches in Coverage %: No
Coverage Strength: 21.17 hits per line

💛 - Coveralls

@jkalberer jkalberer changed the title fix(core): defer sse writehead until first message fix(core): defer sse writehead until after lifecycle completes Apr 10, 2026
SSE returned HTTP 200 on ValidationPipe errors because SseStream.pipe() called writeHead before pipes ran inside the cold interceptor Observable.

- SseStream: defer writeHead to commitHeaders() (and _transform when data flows)
- RouterResponseController.sse(): catchError re-throws before headers commit;
  pipe(..., { end: false }); setTimeout(0) commitHeaders after subscribe
- RouterExecutionContext: pass statusCode and getHeaders() at handle time
- Unit tests and Express/Fastify integration tests (400 before stream opens)
@jkalberer jkalberer force-pushed the fix/sse-pipe-validation-error-status branch from d2cb176 to 457630a Compare April 10, 2026 03:31
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.

2 participants