fix(web): serve SPA shell for dotted deep-link navigations - #879
ritvik-jentic wants to merge 1 commit into
Conversation
710d090 to
d405d02
Compare
Closes #647 Review-board hardening on top of the original shim: - rescued shell is Cache-Control: no-store (same URL answers 404 to non-navigation clients; the rescue path never revalidates) - dot-segment /app paths are refused (never rescued) - middleware-stack-built guard raises RuntimeError instead of a strippable assert - packaging smoke exercises the dotted deep-link against the wheel Signed-off-by: Ritvik <ritvik@jentic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
d405d02 to
dea3acf
Compare
|
Handover status (taking this over from Ritvik):
|
|
Parking this PR for now (merge-ready, CI green) pending a design decision with Manuel. The open question: this shim is ~206 lines of ASGI logic (plus hardening/tests) to work around FastAPI's Options on the table: merge the shim (class fix), switch to the URL-scheme change (instance fix, less code), and/or push the relaxation upstream to FastAPI so the shim can eventually be deleted. |
Summary
Fixes a backend-only bug where browser navigations to a versioned API deep-link
(e.g.
/app/workspace/<vendor>/<name>/1.0) returned raw{"detail":"Not Found"}404 instead of booting the SPA. FastAPI's
app.frontend(fallback="auto")treatsany
/app/*path whose final segment has a file extension as a static-filerequest, and API version segments contain dots (
1.0,2.1.3), so the route wasmis-classified as a missing file. Bookmarks and shared/copied links to versioned
API pages broke for anyone opening them fresh. (The CLI-emitted
approve_urlisan API-namespace path handled by the #813 login redirect, not this shim.)
Related issue
Closes #647
Changes
_SpaNavigationFallbackMiddleware, an innermost ASGI shim that swaps a404 for the SPA shell (200 +
index.html) only for a genuine GET/HEADbrowser navigation under
/app/; everything else passes through untouched soapp.frontend()stays authoritative for real files (304/206/Range/symlinks).mimetypes), not a hardcoded prefix list, so a missing/renamed asset anywherein the bundle (
assets/…or a mount-root file likebroker-openapi.json)still 404s rather than silently booting the app.
back out through request-id/telemetry (carries
x-request-id, logged as thereal 200, not the inner 404).
Acceptheader lines; striproot_pathon asegment boundary; strip
Range/If-Rangefrom the rescued shell (full 200,never 206/416); clean 404 (not 500) if
index.htmlis missing mid-redeploy;idempotent mount that raises
RuntimeErrorif the middleware stack isalready built.
Cache-Control: no-store(the same URL answers 404 to non-navigationclients and the rescue path never revalidates, so keep shared caches out);
dot-segment
/apppaths (/app/../x.1, encoded%2e%2e) are refused —never rescued; packaging smoke now exercises the dotted deep-link and the
no-store/404 split against the installed wheel.
Testing
ruff check,ruff format --check,mypy— clean.uv run pytest tests/unit→ 2142 passed;tests/arch→ 262 passed;make test-integration-sqlite→ 541 passed.1.0,2.1.3); missingasset (incl. root-level
.json/.svg) still 404s;Rangeon the shell → full200; rescued shell carries
x-request-id; splitAcceptstill boots;rescued shell is
no-storewhile the framework-served shell keeps frameworkcaching; dot-segment paths 404; missing
index.htmlmid-redeploy → 404.make smoke-packaging(build UI + wheel, install in clean venv): dotteddeep-link
/app/workspace/stripe-com/stripe-com-api/1.0serves the packagedshell with
no-store, stays 404 for JSON clients, missing recognized assets404 — PASS.
through untouched (
/api/...never rescued),//app,%00, backslash andnon-
/apppaths never rescued, HEAD returns empty body with correctheaders, websocket scopes ignored.
Checklist
git commit -s, DCO)make checkpasses (lint, type check, secrets audit, arch tests)