Skip to content

fix(annotate): scope the framed 404 to paths that name a file, so a VS Code session renders the app - #1565

Merged
backnotprop merged 3 commits into
mainfrom
fix/framed-404-scope
Sep 18, 2026
Merged

backnotprop merged 3 commits into
mainfrom
fix/framed-404-scope

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

Fixes a confirmed high regression from #1561 (1954a19a): an annotate session opened from VS Code shows "404 Not found" instead of the app.

Repro

framedDocumentNotFound (packages/server/html-assets.ts:47, used at packages/server/annotate.ts:1267; identical inline predicate in the Pi mirror apps/pi-extension/server/serverAnnotate.ts:1167) applied the framed-404 document to every path whose request carried Sec-Fetch-Dest: iframe|frame|embed|object — including the app root /. The VS Code extension renders the session URL inside an <iframe> (apps/vscode-extension/src/panel-manager.ts:75) behind its cookie proxy (cookie-proxy.ts:57 forwards headers verbatim), and every subcommand launched from a VS Code terminal is routed there (extension.ts:110 sets PLANNOTATOR_BROWSER). The panel's one auto-reload 404s again, so the session cannot be decided from the editor.

Against origin/main (865d6791), a real annotate server:

$ curl -H 'sec-fetch-dest: iframe' http://127.0.0.1:19711/
<!doctype html>…<p>Not found</p>…   status=404
$ curl http://127.0.0.1:19711/        status=200   (the app)

Plan and review servers are unaffected (no guard).

The rule

One shared predicate in packages/shared/html-assets.ts (vendored to Pi; the Pi mirror now uses it instead of its inline copy):

isFramedEmbeddedDocumentRequest = isFramedFetchDest(dest) && pathNamesEmbeddedDocument(pathname)

pathNamesEmbeddedDocument is true only when the path could name a missing embedded document:

framed path result
/, /?x=1, // app (200)
/settings — one bare segment, no extension app (200)
/prototype-slash.html — last segment has an extension 404 document
/assets/frame — under a directory segment 404 document

A non-framed request for a missing path is untouched and still gets the app, exactly as before #1561. The asset route's own 404 for /api/html-assets/<token>/<missing> is unchanged.

Why the path shape and not Sec-Fetch-Site

An annotated page is a sandboxed srcdoc with an opaque origin, so its nested-document requests carry Sec-Fetch-Site: cross-site — the same value the VS Code webview wrapper produces (and none on both sides for a pasted URL). Site can never separate the two. The path can: the app only ever loads at /, while a relative embed is anchored at /api/html-assets/<token>/ by #1561's <base href> (which has its own 404), so only a root-relative embed reaches the catch-all at all — and that is written as a file reference. A bare single-segment word stays with the app so a future SPA route cannot 404 inside a frame.

Proof

Headless Chromium (Playwright), the QA agent's v-vscode-frame.mjs shape: a cross-origin wrapper page iframing the session URL, and separately through the extension's real createCookieProxy from apps/vscode-extension/src/cookie-proxy.ts. Bun and Pi annotate servers, markdown and raw-HTML sessions:

session root status app rendered
Bun markdown 200 yes
Bun raw HTML 200 yes
Pi markdown 200 yes
Pi raw HTML 200 yes

The #1561 embed fixture (a report with five embedded sibling documents — static src, runtime data-src with ?step=result, <object data>, <embed src>, a sub/deep.html) still renders all five in both runtimes, and a framed missing sibling (/prototype-slash.html, root-relative so it reaches the catch-all) still gets the 404 document naming the file:

"embedsRendered": ["EMBED-1-OK","EMBED-2-OK","EMBED-3-OK","EMBED-4-OK","EMBED-5-OK"],
"missingEmbedText": "prototype-slash.html — Not found",
"missingEmbedStatus": 404

PLANNOTATOR_BROWSER=/usr/bin/true, headless only.

Tests

  • packages/shared/html-assets.test.ts — the predicate: / and /?x=1 are never a missing embed; file references and directory-segment paths are; a bare word is not; isFramedEmbeddedDocumentRequest needs both halves.
  • packages/server/annotate-html-assets.test.ts — the matrix over a real Bun annotate server: framed / and /?x=1 → 200 app HTML; framed /prototype-slash.html → 404 document; framed /assets/frame → 404, framed /settings → app; plain requests for missing paths → app. The existing framedDocumentNotFound unit case stays and gains the root assertion.
  • apps/pi-extension/server/serverAnnotate-embeds.test.ts — the same three cases over the Node transport.

bun run typecheck; bun test packages/shared packages/server apps/pi-extension (1099 / 866 / 291 pass, 0 fail); bun run --cwd apps/review build && bun run build:hook.

Docs

AGENTS.md, "Embedded local documents" → Never the app in a frame: states the exact rule, the / and bare-word exemptions, and why Sec-Fetch-Site is not the signal.

#1561's catch-all guard fired on every framed request, including the app
document at `/`. The VS Code extension frames the session URL, so an
annotate session opened from the editor rendered "404 Not found" instead
of the app.

One shared predicate (pathNamesEmbeddedDocument / isFramedEmbeddedDocumentRequest
in packages/shared/html-assets.ts, vendored to Pi) now decides it on the
SHAPE OF THE PATH, and the Pi mirror uses it instead of its inline copy.
@backnotprop
backnotprop merged commit 1e02fc9 into main Sep 18, 2026
28 checks passed
@backnotprop
backnotprop deleted the fix/framed-404-scope branch September 18, 2026 02:07
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.

1 participant