Skip to content

Prod mode serves HTTP 404 status for direct loads of valid dynamic-route URLs (SPA fallback body renders fine) #6983

Description

@masenf

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]]:

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/.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions