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