Skip to content

Fix SPA dynamic route handling with proper HTTP status codes#6469

Draft
masenf wants to merge 3 commits intomainfrom
claude/investigate-reflex-deployment-YoZvO
Draft

Fix SPA dynamic route handling with proper HTTP status codes#6469
masenf wants to merge 3 commits intomainfrom
claude/investigate-reflex-deployment-YoZvO

Conversation

@masenf
Copy link
Copy Markdown
Collaborator

@masenf masenf commented May 7, 2026

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Description

This PR fixes how the frontend static file server handles Single Page Application (SPA) dynamic routes. Previously, requests to valid dynamic routes (e.g., /blog/[slug]) that didn't have prerendered files would incorrectly return HTTP 404, even though the SPA shell was being served.

Key Changes:

  1. New reflex/utils/frontend.py module: Extracted and enhanced frontend serving logic with:

    • ReflexStaticFiles: A custom Starlette StaticFiles subclass that intelligently distinguishes between:
      • Valid dynamic routes (returns 200 with SPA shell)
      • True 404s (returns 404)
      • Static assets (returns 404 without consulting route resolver)
    • _is_html_navigation(): Helper to identify navigation requests vs. static assets based on file extensions
    • _load_routes_manifest(): Loads route patterns from disk for the standalone frontend server
    • get_frontend_mount(): Creates a properly configured Starlette Mount for frontend serving
  2. Routes manifest system:

    • Added _write_routes_manifest() to reflex/app.py to persist all defined routes at compile time
    • Added ROUTES_MANIFEST constant to track the manifest file location
    • Enables the standalone frontend server to resolve dynamic routes without an App instance
  3. Integration updates:

    • Updated reflex/app.py to pass app.router to get_frontend_mount() for dev server
    • Updated reflex/compiler/compiler.py to write the routes manifest during compilation
    • Moved frontend serving functions from reflex/utils/exec.py to the new reflex/utils/frontend.py
  4. Comprehensive test coverage: Added 11 unit tests covering:

    • Known static routes returning 200
    • Real assets returning 200
    • Dynamic routes returning 200 with SPA shell
    • Unknown routes returning 404
    • Asset misses returning 404 without resolver consultation
    • Resolver receiving normalized paths with leading slashes
    • Routes manifest round-trip serialization

Why This Matters

This fixes a critical issue where dynamic routes in Reflex apps would appear broken when accessed directly (e.g., via URL bar or external links), even though the SPA could navigate to them internally. The fix ensures proper HTTP semantics while maintaining SPA functionality.

Testing

  • Added comprehensive unit tests in tests/units/utils/test_frontend.py
  • Tests cover edge cases: missing assets, dynamic routes, path normalization, manifest loading
  • Existing functionality preserved for static files and prerendered pages

https://claude.ai/code/session_01S2Du3ZAXqyLPfvCYCSPCRU

claude added 3 commits May 7, 2026 17:09
Subclass Starlette's StaticFiles to consult app.router on a 404. When the
path matches a defined dynamic route, rewrite the status to 200 so the SPA
shell renders correctly with a proper status code. True misses still
return 404. Asset extensions skip the route check to avoid CPU overhead.

Refs #6463
…namic routes

Move ReflexStaticFiles to module level (drop the lazy __getattr__ now that
starlette is imported at the top of exec.py). Have the compiler emit a
routes_manifest.json next to stateful_pages.json listing every defined
route pattern; get_frontend_mount falls back to loading it when no
resolver is passed in. This lets the standalone frontend prod app
distinguish dynamic routes from true 404s without an App instance.

Also add tests covering frontend_path prefix mounting and manifest
load/missing/invalid paths.

Refs #6463
Move ReflexStaticFiles, _is_html_navigation, _load_routes_manifest,
get_frontend_mount, and _frontend_prod_app out of reflex/utils/exec.py
into a dedicated reflex/utils/frontend.py module. Update the runner
target string and the call site in app.py.

Hoist the prerequisites import in the renamed test_frontend.py instead
of re-importing it inside each test.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 7, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing claude/investigate-reflex-deployment-YoZvO (620f9c1) with main (2df5344)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

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.

2 participants