From 87c44759a8fcc27bbcae7592ebb3e2b2fddb8ec8 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:06:33 +0800 Subject: [PATCH 1/2] fix(route): don't let a splat catchall match paths sharing its prefix 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) --- reflex/route.py | 4 +++- tests/units/test_route.py | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/reflex/route.py b/reflex/route.py index 4b450fb6e25..bb3dbb53aae 100644 --- a/reflex/route.py +++ b/reflex/route.py @@ -172,7 +172,9 @@ def get_route_regex(keyworded_route: str) -> re.Pattern: # Match a single optional segment (/slug or nothing) regex_parts.append(r"(/[^/]+)?") elif part == constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT: - regex_parts.append(".*") + # The frontend compiles this to React Router's `*`, which matches the + # route and its descendants only, so the segment separator is required. + regex_parts.append("(/.*)?") else: regex_parts.append(re.escape("/" + part)) # Join the regex parts and compile the regex diff --git a/tests/units/test_route.py b/tests/units/test_route.py index fb3f37ef356..ae1b81e89cb 100644 --- a/tests/units/test_route.py +++ b/tests/units/test_route.py @@ -3,7 +3,7 @@ from reflex_base import constants from reflex.app import App -from reflex.route import get_route_args, verify_route_validity +from reflex.route import get_route_args, get_router, verify_route_validity @pytest.mark.parametrize( @@ -110,3 +110,20 @@ def test_check_routes_conflict_valid(mocker: MockerFixture, app, route1, route2) mocker.patch.object(app, "_pages", {route1: []}) # test that running this does not throw an error. app._check_routes_conflict(route2) + + +@pytest.mark.parametrize( + ("path", "expected"), + [ + ("/posts", "posts/[[...splat]]"), + ("/posts/", "posts/[[...splat]]"), + ("/posts/2024/hello", "posts/[[...splat]]"), + ("/postsomething", None), + ("/posts-archive", None), + ], +) +def test_get_router_splat_catchall(path: str, expected: str | None): + # The frontend maps [[...splat]] to React Router's `posts/*`, which matches + # the route and its descendants but not paths that merely share the prefix. + router = get_router(["posts/[[...splat]]"]) + assert router(path) == expected From 3f7e7bb9b6e8c5c35e1acefe19d6723909763e7f Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:08:27 +0800 Subject: [PATCH 2/2] Add changelog fragment for #6790 Co-Authored-By: Claude Opus 4.8 (1M context) --- news/6790.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6790.bugfix.md diff --git a/news/6790.bugfix.md b/news/6790.bugfix.md new file mode 100644 index 00000000000..df9738b98fb --- /dev/null +++ b/news/6790.bugfix.md @@ -0,0 +1 @@ +A `[[...splat]]` catchall route no longer matches paths that merely share its prefix — `posts/[[...splat]]` matched `/postsomething` as well as `/posts` and its descendants, so the wrong page's `on_load` events could fire.