CS-11024: Defer prerender error status to afterRender#4642
Conversation
The prerender server treats `data-prerender-status='error'` on the `[data-prerender]` container as the canonical "DOM is settled, snapshot now" signal. The route-error path was lifting it synchronously before Ember had transitioned to `render.error` and Glimmer had populated the `<pre data-prerender-error>`, so the server could poll between the two writes and capture an empty payload — surfacing the synthesized "invalid error payload" string instead of the actual underlying card error. Split `#applyErrorMetadata` into `#applyErrorMetadataAttrs` (id + nonce, still synchronous) and `#applyErrorStatus` (status flip), and schedule the status flip on the `afterRender` queue from `#processRenderError`. The id/nonce dataset writes that the server uses to correlate captures keep their existing timing. Adds a realm-server prerendering regression test that drives a card whose module throws synchronously during evaluation and asserts the surfaced error contains the underlying throw, not the "invalid error payload" fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 867430698c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Preview deploymentsHost Test Results 1 files ±0 1 suites ±0 1h 56m 55s ⏱️ - 2m 39s Results for commit 27375b8. ± Comparison against earlier commit 74b6e4a. Realm Server Test Results 1 files ± 0 1 suites ±0 18m 4s ⏱️ +55s Results for commit 27375b8. ± Comparison against earlier commit 74b6e4a. |
Codex review caught a downgrade bug: #transitionToErrorRoute's early-failure fallback (when renderBaseParams isn't set yet) writes data-prerender-status='unusable' to force page eviction — renderCaptureToError keys eviction on the 'unusable' literal. The afterRender callback was unconditionally overwriting it to 'error', which meant pages that should have been discarded could be kept and reused. Skip the 'error' write when the current status is already 'unusable'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a prerendering race in the Host app’s render route error handling by deferring the data-prerender-status="error" readiness signal until Ember’s afterRender, ensuring the render.error template has populated the <pre data-prerender-error> payload before the prerender server snapshots it.
Changes:
- Split error metadata writes so
data-prerender-id/data-prerender-nonceare applied synchronously, whiledata-prerender-status="error"is scheduled onafterRender. - Add a realm-server prerender regression test that exercises a module top-level evaluation throw and asserts the captured error surfaces the underlying throw instead of the synthesized “invalid error payload” fallback.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/host/app/routes/render.ts | Defers raising the prerender “error settled” signal to afterRender to avoid capturing an empty error payload. |
| packages/realm-server/tests/prerendering-test.ts | Adds a regression test covering module-evaluation throws to validate the corrected prerender capture ordering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This reverts commit f5e44af.
CI's existing 'errors thrown before the render model hook' test showed the unusable→error downgrade Codex/Copilot flagged is in fact reachable: when beforeModel() throws the runloop is still alive enough for afterRender to fire, so #applyErrorStatus runs after #transitionToErrorRoute's early-failure fallback synchronously wrote data-prerender-status='unusable', overwriting it to 'error' and breaking page eviction. Skip the schedule when renderBaseParams isn't set (the precondition the early-failure fallback gates on). On that path the fallback already writes status='unusable' and the error textContent synchronously, so there's nothing to defer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why this matters
A user-visible "indexing error" surface that hides the real underlying card error has been recurring across multiple realms in production. Every occurrence is a confused user and a noisy support thread; the actual card error (most often a wrong-subpath import or similar module-eval throw) never reaches them.
Repros deterministically on staging for
buck/buck/SprintTask/7d92636a-…jsonacross 4+ separate from-scratch attempts on different days, plus across multiple realms with the same pattern (bn3/bn,lukemelia/testing-2, etc.). All of them surface as the literal worker logrender.html returned an invalid error payload:with empty value after the colon.Mechanism
The prerender server's capture wait condition (
packages/realm-server/prerender/utils.ts:765-779) treats two signals as equivalent — wait is satisfied whendata-prerender-status ∈ {ready, error, unusable}OR the<pre data-prerender-error>has non-empty text:The host is supposed to use
data-prerender-statuson the outer[data-prerender]container as the canonical "DOM is settled, snapshot now" signal. But in#processRenderError(packages/host/app/routes/render.ts), the old#applyErrorMetadatawroteprerenderStatus='error'synchronously, before#transitionToErrorRoutetriggered the Ember transition that renders therender.errortemplate. Until the template renders, the inner<pre data-prerender-error>is either absent or has stale/empty content.Race window:
data-prerender-status='error'written ← prerender wait condition is satisfied here<pre data-prerender-error>render.error{{this.reason}}into the<pre>When the server polls between (1) and (4), the captured value is empty.
JSON.parse('')fails, andrenderCaptureToErrorsynthesizes"render.html returned an invalid error payload: ". The actual card error never reaches the user.The intent — and the contract this PR preserves — is that
data-prerender-status='error'means the DOM is fully settled in an error state, not the route has decided to enter an error state. Today's code raised the signal at decision time rather than at settlement time.Fix
Defer
data-prerender-status='error'to fire after therender.errortemplate has rendered, via Ember'safterRenderqueue. The id/nonce datasets stay where they are — those are immutable for this render and the server doesn't gate on them. Only the status flip moves.#applyErrorMetadatainpackages/host/app/routes/render.tsinto:#applyErrorMetadataAttrs(context)— setsdata-prerender-idanddata-prerender-nonceon[data-prerender]and[data-prerender-error]synchronously. The server uses these for id/nonce parity checks; they're immutable for this render.#applyErrorStatus(context)— setsdata-prerender-status='error'on the outer container.#processRenderErrornow calls#applyErrorMetadataAttrssynchronously, then#transitionToErrorRoute(transition), thenschedule('afterRender', this, this.#applyErrorStatus, context). Glimmer flushes the transitioned-to template's DOM updates beforeafterRenderfires, so by the time we lift the readiness signal the<pre>'s textContent is populated.Acceptance criteria (from CS-11024)
data-prerender-status='error'is set only after[data-prerender-error]'stextContent.trim().length > 0.data-prerender-id/data-prerender-nonceare still set synchronously, so id/nonce-based wait-condition checks continue to work.setError(textContent write) before the status flip viasetStatusToUnusablein the wrapping handler.buck/buck/SprintTask/7d92636a-…jsonindexing run will no longer logrender.html returned an invalid error payload:. Worker error will become the actual underlying error (e.g. the proxyReferenceErrorfor the wrong-subpath import).Out of scope
Test
Adds a realm-server prerendering regression test at
packages/realm-server/tests/prerendering-test.tsthat drives a card whose module throws synchronously during evaluation (eval-throw.gts—throw new Error('module-eval-throw')at top level). Asserts:module-eval-throw),"returned an invalid error payload"fallback,The test exercises the route-error path: module evaluation rejects →
model()rejects → route'serroraction fires with the active transition →#processRenderErrorlifts status='error' → server captures and parses.Test plan
pnpm lint(host + realm-server)realm-server testsshard passescard prerender surfaces errors thrown before the render model hook,card prerender hoists module transpile errors,card prerender surfaces actionable error for bad icon import, etc.)🤖 Generated with Claude Code