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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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