Skip to content

fix: silence streamed Suspense boundary error message in prod#482

Merged
vivek7405 merged 2 commits into
mainfrom
fix/streamed-suspense-error-leak
Jun 12, 2026
Merged

fix: silence streamed Suspense boundary error message in prod#482
vivek7405 merged 2 commits into
mainfrom
fix/streamed-suspense-error-leak

Conversation

@vivek7405

@vivek7405 vivek7405 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #478.

The page-level streaming path (ssr.js streamingHtmlResponse) emitted the raw boundary error message in BOTH dev and prod when a streamed Suspense boundary rejects, leaking internal detail (a DB error, a stack-derived path) to the client. The fix threads the dev flag into streamingHtmlResponse and gates the placeholder: dev surfaces error: <msg> for debugging, prod emits an empty (isolated) boundary.

dev is webjs's authoritative prod signal: the deployment model uses the CLI dev flag, not NODE_ENV (docs/app/docs/deployment/page.ts), and the Dockerfile sets NODE_ENV=production for app code / deps but the server itself keys on dev. So under webjs start (dev: false) this is silent regardless of whether NODE_ENV was exported.

Pre-existing on main (not a regression from #470); narrow to trigger (per-component async-render throws are already isolated, so this catch only fires on a top-level boundary rejection or a non-isolated renderToString throw).

Scope note: this fixes the page-streaming BOUNDARY path. The per-component SSR isolation (render-server.js defaultSSRErrorTemplate) and the core renderToStream boundary catch still key on NODE_ENV (isProd()), so under a bare webjs start with NODE_ENV unset those paths still leak a component error. That broader inconsistency (align all SSR error paths on the dev flag) is tracked separately as #483.

Test plan

  • Integration (packages/server/test/dev/streaming-error-isolation.test.js): a fixture page whose top-level Suspense boundary rejects with a recognizable secret, driven through createRequestHandler. PROD asserts the body does NOT contain the message (still 200, fallback flushed, content rendered); DEV asserts it DOES. Counterfactual verified: neutering the gate fails the PROD case.
  • dev/ + test/ssr/ suites: 346 pass.

Definition of done

  • Tests: integration above. Browser/e2e: N/A because the change is a server-side SSR streaming error path; the only observable difference is the ABSENCE of a leaked message in prod, which the integration test asserts. The blog e2e streaming tests cover the happy path, which is byte-identical (the change is a catch block).
  • Docs / AGENTS / MCP / editor plugins: N/A because no public surface changed.
  • Dogfood: website / docs / ui-website boot 200 in dist mode, no broken preloads. The change cannot affect the happy path (rejection-only catch).
  • Version bump: server is owed a follow-up patch (0.8.22 → 0.8.23) at the next release; not in this PR.
  • Self-review: ran the loop; the one finding (the dev-vs-NODE_ENV inconsistency with the component path) is filed as fix: SSR error prod-silence keys on NODE_ENV not the dev flag, leaks under bare webjs start #483, since aligning the component-isolation path is a separate pre-existing surface.

The page-level streaming path (streamingHtmlResponse) emitted the raw
boundary error message in BOTH dev and prod, leaking internal detail (a
DB error, a stack-derived path) to the client when a streamed Suspense
boundary rejects. This contradicted the framework's own SSR
error-isolation policy (render-server.js defaultSSRErrorTemplate is
prod-silent, and the core renderToStream boundary path already uses it).

Thread the dev flag into streamingHtmlResponse and gate the message: dev
surfaces it for debugging, prod emits an empty (isolated) boundary.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Went back and forth on the signal here. dev is the right one for this path: the deployment model keys prod-silence on the CLI dev flag, not NODE_ENV, so this actually closes the leak under webjs start even when NODE_ENV was never exported. What it does NOT close: the per-component isolation (defaultSSRErrorTemplate) still gates on NODE_ENV, so a bare webjs start with NODE_ENV unset leaks a component error while this boundary path stays silent. The two only line up when NODE_ENV=production is set, which the Dockerfile does but a bare start does not. That is a separate, pre-existing surface, so I filed #483 to align every SSR error path on the dev flag rather than widen this PR. The boundary fix and its prod/dev test stand.

Comment thread packages/server/src/ssr.js
The @param list had drifted (param 1 documented as headHtml but it is
prefix; closer was undocumented). Correct the names and add the missing
entry so the list matches (prefix, bodyHtml, closer, ...nonce, dev).

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Second pass, focused on the test and the plumbing. The test is solid (it asserts the fallback flushed and the content rendered, so an empty body can't false-pass the prod case, and the rejection is awaited so no unhandled-rejection flake), and opts.dev is a real boolean at the call site, so no accidental-falsy trap. One small thing on the JSDoc I was already touching.

Comment thread packages/server/src/ssr.js

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Final pass is clean. The boundary fix sits on the right signal, the JSDoc lines up with the signature now, and the test holds the prod/dev split with a working counterfactual. Ready to merge; the broader component-path alignment is tracked in #483.

@vivek7405 vivek7405 merged commit ff84dd6 into main Jun 12, 2026
7 checks passed
@vivek7405 vivek7405 deleted the fix/streamed-suspense-error-leak branch June 12, 2026 19:15
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.

fix: streamed Suspense boundary error leaks the raw message in prod

1 participant