Repository navigation
Description
Summary
When a prerendered route's render throws, the route's error boundary catches it and rango bakes the rendered error page as the prerender output, reporting OK and letting the build succeed. At runtime the baked error page is served as a normal HTTP 200. The failure is completely silent: status checks/CI/smoke tests see 200 + valid HTML, so a fully-broken page looks healthy. You only notice by eyeballing the page.
This bit me on a real deploy: every store category page (/shop/new-arrivals, …) served a "Something Went Wrong" error boundary as a 200.
Repro
preset: "vercel", aPrerender(...)route whose render fn readsctx.env(here: building an SFCC client).buildEnvleft at its default (off)."auto"is Cloudflare-only, so on the vercel preset there's no build-time env.- At build, the render fn throws
ctx.env is not available during pre-rendering. - The router's
defaultErrorBoundarycatches it and renders the error UI. - That error UI is baked into the prerender manifest and served at runtime (200,
x-vercel-cache: MISS).
Minimal shape:
export const CategoryPage = Prerender(
async () => [/* paths */],
async (ctx) => {
const commerce = await makeClient(ctx.env); // throws during prerender when buildEnv is off
// ...render
},
);Root cause
packages/rangojs-router/src/vite/discovery/prerender-collection.ts, the render loop in expandPrerenderRoutes:
routerInstance.matchForPrerender(...)is awaited (around L249).- On a returned
result, the segments/handles are staged into the prerender manifest and loggedOK(around L268–298) — unconditionally, with no inspection of whether the render resolved via an error boundary. - The
catchthat logsFAILand re-throws to fail the build (around L299–328) only fires ifmatchForPrerenderthrows.
But a render error is swallowed by the route's error boundary inside matchForPrerender, which then returns a normal result (the error-boundary segments). So the loop never sees a throw, logs OK, and bakes the error page.
Tellingly, the router's onError does fire during this prerender (the build logs Unhandled error in <app> router: { error: Error: ctx.env is not available during pre-rendering ... }). So the framework already detects the error — it just doesn't let that affect the prerender result or the build's exit status.
Why it's bad
- Silent, user-visible breakage. Broken pages ship as 200 with valid HTML; CI/status-only checks pass. The build also exits 0.
- Misleading, because the "fail the build on prerender error" path already exists — it just doesn't trigger, since the error never reaches it.
- It's specifically nasty with prerender because the bad output is frozen at build and served verbatim; the runtime function (where
ctx.envwould have existed) never even runs for those URLs.
Proposed fix (options)
- Surface error-boundary use from
matchForPrerender. Have it report when a render resolved via an error boundary /onErrorfired (e.g.result.erroredor anerrorfield), and in the prerender loop treat that like the existingFAILpath: don't bake it, and either fail the build or fall back to dynamic. - Don't apply the error boundary during prerender (or render in a "throw-on-error" mode), so a render error propagates to the loop's existing
catch→FAIL→ build fails. This reuses the machinery that's already there. - At minimum, make it loud and configurable: a non-zero build exit (or a
prerender: { onError: "fail" | "dynamic" | "bake" }knob) instead of silently baking, plus a warning that lists the URLs that prerendered via an error boundary.
Falling back to dynamic (option 1/2) is especially appropriate here: at runtime ctx.env exists, so these pages would have rendered fine if they hadn't been frozen as build-time errors.
Environment
@rangojs/[email protected](dist-tagpreview)preset: "vercel", host router, Node 24 / Vite 8
Found while dogfooding the vercel preset in the rsc-cloudflare-app testbed (full write-up: RANGO-ISSUE-vercel-host-router.md, finding #3). Workaround that fixed it consumer-side: pass a buildEnv so prerender render fns can read ctx.env — but the silent-bake behavior is the real bug.
Activity
fix(router): surface prerender render errors instead of baking them (#…
fix(router): fail or skip a prerendered route whose component throws …
fix(router): fail or skip a prerendered route whose component throws …
fix(router): fail or skip a prerendered route whose component throws …
feat(router): on-demand (ISR-style) prerender (#640)
feat(router): router.prerender() warms any route before traffic (#1063)