Skip to content

fix(route): don't let a splat catchall match paths sharing its prefix - #6790

Merged
masenf merged 3 commits into
reflex-dev:mainfrom
chuenchen309:fix/splat-catchall-prefix-match
Aug 7, 2026
Merged

fix(route): don't let a splat catchall match paths sharing its prefix#6790
masenf merged 3 commits into
reflex-dev:mainfrom
chuenchen309:fix/splat-catchall-prefix-match

Conversation

@chuenchen309

Copy link
Copy Markdown
Contributor

Problem

get_route_regex compiles the splat catchall to a bare .* with no separator in front of it, while every other segment type prepends /. So posts/[[...splat]] becomes ^/posts.*/?$, which matches any path that merely starts with the literal text:

>>> from reflex.route import get_router
>>> router = get_router(["posts/[[...splat]]"])
>>> router("/posts")            # 'posts/[[...splat]]'  — correct
>>> router("/postsomething")    # 'posts/[[...splat]]'  — wrong
>>> router("/posts-archive")    # 'posts/[[...splat]]'  — wrong

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 to posts/*, which matches /posts and 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.router feeds get_load_events (app.py:1038), called per page load from state.py:2467, and app.py:1804 (self.app.router(path) or "404"). A path resolving to the wrong route means a different page's on_load events fire than the page that renders.

Fix

.*(/.*)?, making the separator required while keeping the zero-segment case (/posts itself) 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_catchall in tests/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 --check clean, uv run pyright reflex tests → 0 errors. (tests/units/istate/manager/test_expiration.py is flaky on main independently 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.

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>
@chuenchen309
chuenchen309 requested a review from a team as a code owner July 17, 2026 01:08
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a regex mismatch between the Python route matcher and the React Router frontend: [[...splat]] catchall segments were compiled to .* (no separator required), allowing posts/[[...splat]] to wrongly match /postsomething. The fix replaces .* with (/.*)?, enforcing a / separator while keeping the zero-segment case.

  • reflex/route.py: One-line change in get_route_regex.*(/.*)? for DOUBLE_CATCHALL_SEGMENT, with an explanatory comment tying it back to React Router semantics.
  • tests/units/test_route.py: Five-case parametrized test (test_get_router_splat_catchall) covering exact match, trailing slash, descendant path, and both prefix false-positive cases.

Confidence Score: 5/5

  • Safe to merge — the change is a minimal, well-targeted regex correction with no side-effects on other route types.
  • The one-line change is provably correct: (/.*)? enforces a / separator before the wildcard, exactly mirroring React Router's posts/* semantics. All three other segment types (SINGLE_SEGMENT, DOUBLE_SEGMENT, literal parts) are untouched. The new test covers both valid matches and the two prefix false-positives that triggered the bug.
  • No files require special attention.

Important Files Changed

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

@codspeed-hq

codspeed-hq Bot commented Jul 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing chuenchen309:fix/splat-catchall-prefix-match (d02d837) with main (4116ff2)2

Open in CodSpeed

Footnotes

  1. 8 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.

  2. No successful run was found on main (d8a132b) during the generation of this report, so 4116ff2 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@masenf
masenf merged commit d0b98a3 into reflex-dev:main Aug 7, 2026
108 checks passed
masenf pushed a commit that referenced this pull request Aug 28, 2026
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
masenf pushed a commit that referenced this pull request Aug 28, 2026
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
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.

3 participants