Skip to content

fix(studio-server): a refused preview frame says so at once instead of timing out - #6074

Open
miguel-heygen wants to merge 2 commits into
mainfrom
drefall/shadow-reload-runtime
Open

miguel-heygen wants to merge 2 commits into
mainfrom
drefall/shadow-reload-runtime

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

When the preview server refuses a frame's request (the project folder is gone, or ?variables= is invalid), the frame now gets a small page that reports the refusal, instead of a JSON body.

Why

The shadow reload waits on the new document's runtime. A refused frame loads a JSON body, which has no runtime and raises no error, so nothing tells the player to give up: Studio waits all three 15 s budgets and then reports "The preview did not reload (it took too long to load). It was still waiting for the page and its runtime to load." The old preview stays on screen and edit thumbnails stay held for the whole wait.

Reproduced in Chromium with the built player (no-load-timeout, as the shadow uses it): a JSON 404 gives no error event and no __player for 20 s.

Related work

Refs the "preview reload timed out, waiting for runtime" reports. This covers the refused-frame cause only. A page whose own parser-blocking scripts or stylesheets stall also never boots a runtime (reproduced in the same harness); that is a separate cause and is not changed here.

How

createStudioApi gets one outer middleware on /projects/:id/preview and /preview/*: when the browser marks the request as a frame navigation (Sec-Fetch-Dest: iframe) and the answer is an error, the body becomes a page that seeds window.__hfPreviewErrors. The player's composition probe already reads that list, so it raises its error event and the shadow fails at once with the status named. Requests that are not frame navigations keep their JSON.

Test plan

  • Unit tests added/updated: previewErrorDocument.test.ts (frame request gets the page, sub-composition frame gets it, plain fetch keeps its JSON). The two frame tests fail without the middleware.
  • Manual testing performed: real createStudioApi behind Chromium with the built player: the error event arrives after about 0.5 s with "The preview page could not be loaded (HTTP 404)."; before, it never arrived.
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments"

studio-server preview and path-safety suites (171 tests), tsc, oxlint, oxfmt and fallow audit pass on the devbox.

…f timing out

A frame the server refuses (project gone, bad variables) loaded a JSON body: no runtime and no error, so Studio waited three 15s budgets and reported 'waiting for runtime'. The refusal now reaches the frame as a page that reports it, like a failing preview script.
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 154 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • seqplainkeys-none-pct-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@terencechotai2-commits terencechotai2-commits left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approving at 3f02569d. I found no blocker.

Checked

  • Order. answerRefusedFrameWithPage is registered before answerProjectDirMissingAfterErrorHandlers, so it runs outermost. It rewrites the answer after the folder-gone replacement and after onError.
  • Reach. It applies only to /projects/:id/preview and the paths under it, and only when Sec-Fetch-Dest: iframe and the status is 400 or above. A runtime fetch or an <img> asset request under the same path keeps its JSON.
  • The player's side. The page seeds window.__hfPreviewErrors, the same key EARLY_ERRORS_SCRIPT sets on a served preview. CompositionProbe reads it through firstAuthorError before it checks for a runtime, so the shadow fails on the first probe. Studio's useConsoleErrorCapture reads the same list.
  • Injection. The only variable in the page is the numeric status, put in through JSON.stringify, so nothing a request controls reaches the script or the <p>.
  • Headers. The original headers are copied, content-type is set to HTML, and content-length is dropped for the new body. Neither studio-server nor the CLI server sets a CSP that would block the inline script.

Probes I ran (temporary test file, since removed). Each one was an iframe request through createStudioApi:

  • A resolved project whose folder is gone: 404 page, on the preview and on a sub-composition.
  • ?variables= that isn't valid JSON: 400 page.
  • An unknown project's asset path: 404 page.
  • An asset path in the folder-gone project as Sec-Fetch-Dest: image: still 404 JSON, {"error":"not found","why":"project_dir_missing"}.

CI. Every check on this head passed or was skipped, including the 11 required ones: Windows tests and render, all five edit accuracy shards and their gate, captures, typecheck and build.

Run locally

  • vitest run src/routes/previewErrorDocument.test.ts src/routes/preview.test.ts in studio-server: 117/117.
  • Mutant: without the two api.use lines, the two frame tests fail and the JSON test passes.

Non-blocking

  • The reason is dropped. The page says only "HTTP 404" or "HTTP 400". The JSON it replaces can say why, for example why: "project_dir_missing" for a folder that's gone. Carrying that reason into the message would let Studio say "the project folder is gone" rather than give a bare status.

— tai2

This branch has not been deployed

No deployments
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