fix(route): don't let a splat catchall match paths sharing its prefix - #6790
Conversation
The catchall segment compiled to a bare `.*` with no separator, so `posts/[[...splat]]` became `^/posts.*/?$` and matched `/postsomething`. The frontend maps the same route to React Router's `posts/*`, which matches the route and its descendants only, so a request could resolve to a different page's on_load events than the page actually rendered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR fixes a regex mismatch between the Python route matcher and the React Router frontend:
Confidence Score: 5/5
|
| Filename | Overview |
|---|---|
| reflex/route.py | Changes .* to (/.*)? in the splat catchall regex branch so that posts/[[...splat]] no longer matches paths that merely share the prefix (e.g. /postsomething). The fix is correct and minimal. |
| tests/units/test_route.py | Adds test_get_router_splat_catchall with five parametrized cases covering the exact-match, descendant-match, and prefix-false-positive scenarios. Good coverage of the bug and fix. |
| news/6790.bugfix.md | New changelog entry describing the bug and the fix. |
Reviews (2): Last reviewed commit: "Merge branch 'main' into fix/splat-catch..." | Re-trigger Greptile
Merging this PR will not alter performance
Comparing Footnotes
|
Sample apps, Playwright drivers, NOTES.md and evidence screenshots from end-to-end browser testing of the routing fixes (#6593 on_load supersedes, #6790 splat matching, #6953 static/dynamic siblings, #6919 chained-event routing) and the rx.memo changes (#6949 call-site auto-memoization, #6605 RestProp style classification, #6945 displayName, #6730 wrapper=). Both clusters pass in dev and prod against the published PyPI alphas, with 0.9.8 baseline comparisons reproducing the old bugs. Anomalies recorded in each NOTES.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
A ~29s 1920x1080 motion graphic covering the end-user-facing changes in 0.9.9, sourced from the 0.9.9a1 and 0.9.9a2 changelog entries across reflex, reflex-base, the component packages and reflex-hosting-cli. Nine beats: the dev-mode main-thread win (#6905), call-site auto-memoization of @rx.memo (#6949), the move to React Router 8 (#6854), DevTools component naming (#6945), stdlib logging with --json plus the client_error report (#6863, #6865, #6827), the cloud CLI's whoami/token and autoscaling flags (#6918, #6884, #6948), and a grid of the smaller fixes a user would notice (#6786, #6593, #6944, #6971, #6790, #6920). Styled to match reflex.dev by reusing its design system rather than approximating it: the dark violet/slate scales, wave-line motif, radial bloom and type scale come from reflex-site-shared, and the fonts are the same @fontsource-variable Instrument Sans and JetBrains Mono files the site serves. Source only — the rendered mp4 is a build artifact, regenerated by `npm run build` and ignored. Lives outside the packaged tree (hatch only includes /reflex), so it does not affect the published wheel or sdist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016arkVKmoogzUtZNfZZSvSo
Problem
get_route_regexcompiles the splat catchall to a bare.*with no separator in front of it, while every other segment type prepends/. Soposts/[[...splat]]becomes^/posts.*/?$, which matches any path that merely starts with the literal text:Why the second two are wrong
Not a judgement call — the frontend disagrees with it.
_embed_manifest_path_for(packages/reflex-base/src/reflex_base/plugins/embed.py:42-73) maps[[...splat]]to React Router's*, so the same route compiles toposts/*, which matches/postsand its descendants but not/postsomething. The Python router and the router that actually renders the page were answering differently for the same path.Impact
app.routerfeedsget_load_events(app.py:1038), called per page load fromstate.py:2467, andapp.py:1804(self.app.router(path) or "404"). A path resolving to the wrong route means a different page'son_loadevents fire than the page that renders.Fix
.*→(/.*)?, making the separator required while keeping the zero-segment case (/postsitself) matching.I probed the other segment types against React Router's semantics before changing anything — single, optional, and index all already agree, so the catchall was the only divergence.
Tests
test_get_router_splat_catchallintests/units/test_route.py, parameterized over both the matching and non-matching cases. Verified red before the fix (the two prefix cases fail, the three legitimate ones pass), green after.uv run pytest tests/units→ 4340 passed.uv run ruff check ./ruff format --checkclean,uv run pyright reflex tests→ 0 errors. (tests/units/istate/manager/test_expiration.pyis flaky onmainindependently of this change — it fails ~2 runs in 5 on a pristine checkout.)AI assistance
Written with Claude Code. I verified the repro, the React Router mapping, and the pre-existing flake myself rather than taking them on the model's word, and I'm responsible for the contents.