diff --git a/news/+route-case-insensitive.bugfix.md b/news/+route-case-insensitive.bugfix.md new file mode 100644 index 00000000000..13204c47085 --- /dev/null +++ b/news/+route-case-insensitive.bugfix.md @@ -0,0 +1 @@ +Match page routes case-insensitively on the backend, as React Router does on the frontend, so `on_load` events and `router.page.path` resolve for a URL like `/About` instead of falling back to the 404 page. diff --git a/reflex/route.py b/reflex/route.py index afd2c5fde94..2fa0ef3d635 100644 --- a/reflex/route.py +++ b/reflex/route.py @@ -180,7 +180,8 @@ def get_route_regex(keyworded_route: str) -> re.Pattern: # Join the regex parts and compile the regex regex_pattern = "".join(regex_parts) regex_pattern = f"^{regex_pattern}/?$" - return re.compile(regex_pattern) + # React Router matches route paths case-insensitively by default. + return re.compile(regex_pattern, re.IGNORECASE) def get_router(routes: list[str]) -> Callable[[str], str | None]: diff --git a/tests/units/test_route.py b/tests/units/test_route.py index 3e51a5b1bbe..a3c72a490d0 100644 --- a/tests/units/test_route.py +++ b/tests/units/test_route.py @@ -135,6 +135,23 @@ def test_get_router_splat_catchall(path: str, expected: str | None): assert router(path) == expected +@pytest.mark.parametrize( + ("path", "expected"), + [ + ("/About", "about"), + ("/ABOUT/", "about"), + ("/Posts/42", "posts/[id]"), + ("/team", "Team"), + ("/", "index"), + ], +) +def test_get_router_ignores_case(path: str, expected: str): + # React Router renders a page for any casing of its path, so the backend + # must resolve the same route to run its on_load events. + router = get_router(["index", "about", "posts/[id]", "Team"]) + assert router(path) == expected + + @pytest.mark.parametrize( ("path", "expected"), [