Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6790.bugfix.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion reflex/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion tests/units/test_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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