You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In self-hosted prod (reflex run --env prod), a direct load of any dynamic-route URL returns HTTP status 404 with the SPA-fallback HTML as the body; the page then renders correctly client-side. Valid dynamic URLs and genuinely unknown paths are indistinguishable at the HTTP layer (byte-identical bodies, same status). Static routes behave differently: they 307-redirect to .../ and then 200.
Filing this as a behavior/docs question rather than a straight bug: is the 404 status for valid dynamic routes intended (accepted SPA-fallback tradeoff to be documented as a known limitation), or should the status code be fixed? Bad for SEO, uptime monitors, and anything that trusts status codes.
To Reproduce
App with pages /, /other, /articles/[id], /posts/[[...splat]]:
reflex run --env prod --frontend-port 8604 --backend-port 8604
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8604/ # 200
curl -sS -o /dev/null -w '%{http_code}\n' -L http://localhost:8604/other # 307 -> 200
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8604/articles/7 # 404 (renders fine in browser)
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8604/posts/a/b # 404 (renders fine in browser)
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8604/definitely-not-a-page # 404, same body
Browser check (document response status): /articles/7 -> status 404, renders the correct article page.
Observed behavior
All dynamic-route direct loads return status 404 with a body byte-identical to .web/build/client/404.html; true 404s return the same thing.
Expected behavior
Valid, routable URLs return 200 (the prod backend knows the route table — app.router(path) — so the static mount could serve the fallback with 200 for routable paths and reserve 404 for genuinely unknown ones); or the current behavior is documented as a known limitation of the SPA-fallback serving model.
Root cause (from source)
Prod mode mounts the built frontend with Starlette StaticFiles(html=True) (get_frontend_mount -> PrecompressedStaticFiles), and the build step copies the react-router SPA fallback (__spa-fallback.html) over 404.html (build.py: path_ops.cp(spa_fallback, static_dir / "404.html")). Starlette's html=True semantics serve 404.html with status 404 for any path that maps to no file; dynamic routes are never prerendered to files, so they always take this path. Static pages exist as <route>/index.html dirs, hence the 307 -> 200.
Specifics (please complete the following information):
Python Version: 3.11
Reflex Version: 0.9.9a1
OS: linux
Browser (Optional): Chromium
Additional context
Pre-existing: identical curl/browser matrix on 0.9.8 — NOT a 0.9.9 regression.
Closely related to Server returns 200 (soft 404) for non-existent routes instead of a real 404 #6463 (soft 404: 200 for non-existent routes) — same underlying "SPA fallback does double duty" design; this is the mirror-image symptom on the self-hosted StaticFiles(html=True) path, and a route-table-aware fix would resolve both.
Found during 0.9.9a1 pre-release testing; repro app and status matrices are on branch claude/reflex-prerelease-testing-t0sd90 under prerelease-testing/2026-08-27-v0.9.9a1/routing/.
Describe the bug
In self-hosted prod (
reflex run --env prod), a direct load of any dynamic-route URL returns HTTP status 404 with the SPA-fallback HTML as the body; the page then renders correctly client-side. Valid dynamic URLs and genuinely unknown paths are indistinguishable at the HTTP layer (byte-identical bodies, same status). Static routes behave differently: they 307-redirect to.../and then 200.Filing this as a behavior/docs question rather than a straight bug: is the 404 status for valid dynamic routes intended (accepted SPA-fallback tradeoff to be documented as a known limitation), or should the status code be fixed? Bad for SEO, uptime monitors, and anything that trusts status codes.
To Reproduce
App with pages
/,/other,/articles/[id],/posts/[[...splat]]:Browser check (document response status):
/articles/7-> status 404, renders the correct article page.Observed behavior
All dynamic-route direct loads return status 404 with a body byte-identical to
.web/build/client/404.html; true 404s return the same thing.Expected behavior
Valid, routable URLs return 200 (the prod backend knows the route table —
app.router(path)— so the static mount could serve the fallback with 200 for routable paths and reserve 404 for genuinely unknown ones); or the current behavior is documented as a known limitation of the SPA-fallback serving model.Root cause (from source)
Prod mode mounts the built frontend with Starlette
StaticFiles(html=True)(get_frontend_mount->PrecompressedStaticFiles), and the build step copies the react-router SPA fallback (__spa-fallback.html) over404.html(build.py: path_ops.cp(spa_fallback, static_dir / "404.html")). Starlette'shtml=Truesemantics serve404.htmlwith status 404 for any path that maps to no file; dynamic routes are never prerendered to files, so they always take this path. Static pages exist as<route>/index.htmldirs, hence the 307 -> 200.Specifics (please complete the following information):
Additional context
StaticFiles(html=True)path, and a route-table-aware fix would resolve both.claude/reflex-prerelease-testing-t0sd90underprerelease-testing/2026-08-27-v0.9.9a1/routing/.