Skip to content

fix: SSR error prod-silence keys on the dev flag, not NODE_ENV#484

Merged
vivek7405 merged 5 commits into
mainfrom
fix/ssr-error-prod-silence
Jun 12, 2026
Merged

fix: SSR error prod-silence keys on the dev flag, not NODE_ENV#484
vivek7405 merged 5 commits into
mainfrom
fix/ssr-error-prod-silence

Conversation

@vivek7405

Copy link
Copy Markdown
Collaborator

Summary

Closes #483.

defaultSSRErrorTemplate (the per-component SSR error isolation) and the core renderToStream boundary catch gated prod-silence on NODE_ENV. But webjs keys prod on the CLI dev flag (the deployment docs say so), and webjs start does NOT export NODE_ENV=production, so a bare prod launch leaked a thrown component's error message (a DB error, a stack-derived path) to the client. This also made #482's "the two streaming error paths now agree" only true when NODE_ENV=production was set.

The fix threads the server dev flag through the SSR render context: ssr.js stamps it on the suspenseCtx; renderToString / renderToStream source it (opts.dev wins, else the ctx) and back-fill the ctx so a streamed sub-render inherits it, then thread it to injectDSD and the boundary catch. Prod-silence now triggers whenever dev is false, regardless of NODE_ENV. A context-free renderToString with no dev signal falls back to NODE_ENV, preserving the prior behaviour for a bare unit/embedded render. The component-isolation path, the page-streaming boundary, and the core renderToStream boundary now agree on the same signal.

Test plan

  • Integration (packages/server/test/dev/ssr-error-prod-silence.test.js): a component whose render() throws, driven through createRequestHandler with NODE_ENV unset. PROD (dev:false) emits NO message (still isolates, siblings render); DEV (dev:true) surfaces it via the isolation template (data-webjs-error); a context-free renderToString falls back to NODE_ENV. Counterfactual verified: neutering the dev-gating makes the PROD case leak.
  • Full node suite 2397 pass; core render/suspense + server dev + test/ssr 493 pass (caught and fixed a regression where loadingTemplates's local dev param was wrongly referenced as opts.dev).

Definition of done

  • Tests: integration above. Browser/e2e: N/A because the change is server-side SSR error rendering; the observable difference is the ABSENCE of a leaked message in prod, asserted by the integration test. The blog e2e (82) and the streaming/suspense suites cover the happy path, which is byte-identical.
  • Docs: N/A because the user-facing docs (error-handling, components.md) say "dev"/"prod", not "NODE_ENV", so they were aspirationally correct; this fix makes the "silent in prod" claim actually true under webjs start. The deployment doc already documents that webjs keys on the dev flag.
  • MCP / editor plugins: N/A because no introspection surface, rule, or grammar changed.
  • Dogfood: blog e2e 82 pass; website / docs / ui-website boot 200 in dist mode, no broken preloads. (Note: @webjsdev/core dist is gitignored and CI rebuilds it from src; I rebuilt it locally so the createRequestHandler path exercises the fix.)
  • Version bump: core + server are owed a follow-up patch at the next release; not in this PR.

@vivek7405 vivek7405 self-assigned this Jun 12, 2026

@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 through the threading end to end. The fix is right and the back-fill of ctx.dev is safe (request-scoped ctx, only set when undefined). Two things on the record. First, my original integration test passed for the wrong reason: under createRequestHandler the render effectively sees NODE_ENV=production, so reverting the dev-gating did not make the PROD case leak, only the DEV case flipped. The real counterfactual lives at the renderToString({suspenseCtx:{dev}}) layer with NODE_ENV deleted, which I added. Second, processSuspenseElements was not getting the dev flag, so a throw inside an inline boundary on a ctx-absent render fell back to NODE_ENV (masked by a double-render). Both fixed.

Comment thread packages/core/src/render-server.js
Comment thread packages/server/test/dev/ssr-error-prod-silence.test.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.

Second look at the threading completeness. Caught that my earlier processSuspenseElements fix never actually landed: a counterfactual git checkout reverted it before I committed, so the helper was still re-deriving ctx && ctx.dev instead of taking the dev param. That left a throwing component inside an inline (ctx-absent) falling back to NODE_ENV (masked only by the outer walk re-rendering it, which also wastes a render). Re-applied it and unified BOTH suspense helpers (processSuspenseElements + streamSuspenseBoundaries) onto the threaded dev param, so they can no longer diverge from the walk.

Comment thread packages/core/src/render-server.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.

Last pass: the dev threading is complete across all four error paths (the walk, an inline non-streaming boundary, a streamed boundary, and a rejected streamed boundary), and the only remaining ctx.dev reads are the legitimate back-fills. Two JSDoc blocks (injectDSD + streamSuspenseBoundaries) had not documented the new dev param; filled those in.

Comment thread packages/core/src/render-server.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 dev flag threads through every SSR error path now (the walk, inline and streamed boundaries, and a rejected streamed boundary), the back-fill is the only ctx.dev read and is request-scoped, and the core-layer counterfactual genuinely fails if the gating is removed. Ready to merge.

t added 5 commits June 13, 2026 00:46
defaultSSRErrorTemplate (the per-component SSR error isolation) and the
renderToStream boundary catch gated prod-silence on NODE_ENV. But webjs
keys prod on the CLI dev flag, and webjs start does not export
NODE_ENV=production, so a bare prod launch leaked a component error
message to the client.

Thread the server dev flag through the SSR render context (ssr.js sets
it on the suspenseCtx; renderToString/renderToStream back-fill and thread
it to injectDSD and the boundary catch). Prod-silence now triggers
whenever dev is false. A context-free renderToString with no dev signal
falls back to NODE_ENV, preserving the prior behaviour for that path. The
component-isolation path, the page-streaming boundary, and the core
renderToStream boundary now agree on the same signal.
loadingTemplates(route, ctx, dev) has a dev parameter, not opts. The
prod-silence dev threading wrongly referenced opts.dev there, throwing a
ReferenceError that the per-file catch swallowed, dropping the
wj-loading template. Use the dev param.
…actual

processSuspenseElements did not receive the dev flag, so a throwing
component inside an inline (ctx-absent) <webjs-suspense> reached
defaultSSRErrorTemplate with dev=undefined, falling back to NODE_ENV (a
latent leak under bare webjs start, masked only by a redundant
double-render). Thread dev through it.

Add a core-layer renderToString({suspenseCtx:{dev}}) test with NODE_ENV
unset: dev:false stays silent (a genuine leak counterfactual that fails
when the dev-gating is reverted), dev:true surfaces. The createRequestHandler
NODE_ENV is effectively production during a render, so the integration
tests alone could not prove the prod-leak prevention.
Re-apply (and complete) the dev threading: processSuspenseElements and
streamSuspenseBoundaries now take dev and use it for injectDSD and the
boundary defaultSSRErrorTemplate, instead of re-deriving ctx && ctx.dev.
This silences a throwing component inside an inline (ctx-absent)
<webjs-suspense> in prod (the ctx.dev derivation was undefined there, so
it fell back to NODE_ENV), and removes the walk-dev-vs-ctx.dev divergence
the suspense helpers had.
@vivek7405 vivek7405 force-pushed the fix/ssr-error-prod-silence branch from bdb3a32 to c33da2d Compare June 12, 2026 19:16
@vivek7405 vivek7405 merged commit e0d2719 into main Jun 12, 2026
@vivek7405 vivek7405 deleted the fix/ssr-error-prod-silence branch June 12, 2026 19:17
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: SSR error prod-silence keys on NODE_ENV not the dev flag, leaks under bare webjs start

1 participant