diff --git a/packages/reflex-base/src/reflex_base/config.py b/packages/reflex-base/src/reflex_base/config.py index 9a522628e6a..9ff2895f5ce 100644 --- a/packages/reflex-base/src/reflex_base/config.py +++ b/packages/reflex-base/src/reflex_base/config.py @@ -348,6 +348,9 @@ def _post_init(self, **kwargs): for key, env_value in env_kwargs.items(): setattr(self, key, env_value) + # Normalize route prefixes to ensure they start with a slash. + self._normalize_paths() + # Normalize disable_plugins: convert strings and Plugin subclasses to instances. self._normalize_disable_plugins() @@ -418,6 +421,14 @@ def _normalize_disable_plugins(self): ) self.disable_plugins = normalized + def _normalize_paths(self): + """Ensure frontend and backend paths start with a slash if provided.""" + if self.frontend_path and not self.frontend_path.startswith("/"): + self.frontend_path = f"/{self.frontend_path}" + + if self.backend_path and not self.backend_path.startswith("/"): + self.backend_path = f"/{self.backend_path}" + def _add_builtin_plugins(self): """Add the builtin plugins to the config.""" for plugin in _PLUGINS_ENABLED_BY_DEFAULT: diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 0889e9359e1..451607e9bb2 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -13,7 +13,6 @@ from collections.abc import Sequence from pathlib import Path from typing import Any, NamedTuple, TypedDict -from urllib.parse import urljoin from reflex_base import constants from reflex_base.config import get_config @@ -227,8 +226,6 @@ def run_process_and_launch_url( if match: if first_run: url = match.group(1) - if get_config().frontend_path != "": - url = urljoin(url, get_config().frontend_path) notify_frontend(url, backend_present) if backend_present: diff --git a/tests/integration/tests_playwright/test_frontend_path.py b/tests/integration/tests_playwright/test_frontend_path.py index 8434938a894..e4e52aba22b 100644 --- a/tests/integration/tests_playwright/test_frontend_path.py +++ b/tests/integration/tests_playwright/test_frontend_path.py @@ -214,8 +214,14 @@ def bouncer_dynamic(): @pytest.fixture( scope="module", - params=["", "/prefix", "/prefix/nested"], - ids=["no-prefix", "single-level", "two-level"], + params=["", "/prefix", "/prefix/nested", "noslash", "noslash/nested"], + ids=[ + "no-prefix", + "single-level", + "two-level", + "noslash-single-level", + "noslash-two-level", + ], ) def frontend_path(request: pytest.FixtureRequest) -> str: """Parametrise over no-prefix and various prefix depths. @@ -450,3 +456,23 @@ def test_navigate_back_and_forth(frontend_path_app: AppHarness, page: Page): expect(log).to_contain_text("index") expect(log).to_contain_text("static") expect(log).to_contain_text("dynamic-7") + + +def test_frontend_url_format(frontend_path_app: AppHarness, frontend_path: str): + """Verify that the frontend_url correctly incorporates the frontend_path.""" + url = frontend_path_app.frontend_url + assert url is not None + + from urllib.parse import urlparse + + parsed = urlparse(url) + + expected_path = frontend_path + if expected_path and not expected_path.startswith("/"): + expected_path = f"/{expected_path}" + if expected_path and not expected_path.endswith("/"): + expected_path = f"{expected_path}/" + if not expected_path: + expected_path = "/" + + assert parsed.path == expected_path