From 34535c44078165a13f084a3eeed7bfe0a3d5d53c Mon Sep 17 00:00:00 2001 From: ChinmayShringi Date: Sun, 3 May 2026 13:56:47 -0400 Subject: [PATCH 1/2] fix(exec): correct frontend URL when frontend_path lacks leading slash (#6360) Setting REFLEX_FRONTEND_PATH=noslash (no leading slash) caused startup to print: App running at: http://localhost:3001/noslash/noslash/ Root cause: urllib.parse.urljoin treats a relative path differently from an absolute one. Vite already prints the listening URL with the configured base appended (e.g. http://localhost:3001/noslash/), and joining a relative "noslash" against that doubled the segment, while a "/noslash" string would have replaced it. Extract _apply_frontend_path() that normalizes the configured path to "/path/" form before joining, so the result is independent of how the user wrote frontend_path. Add a parametrized unit test covering all slash variants and the multi-segment case. Closes #6360 --- reflex/utils/exec.py | 29 ++++++++++++++++++++--- tests/units/utils/test_exec.py | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tests/units/utils/test_exec.py diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 0889e9359e1..7b62147409f 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -159,6 +159,29 @@ def notify_frontend(url: str, backend_present: bool): ) +def _apply_frontend_path(base_url: str, frontend_path: str) -> str: + """Apply ``frontend_path`` to a base URL emitted by the frontend dev server. + + ``urljoin`` treats a relative path differently from an absolute one, so + a ``frontend_path`` configured without a leading slash (e.g. ``"app"``) + used to produce duplicated path segments like + ``http://localhost:3001/app/app/`` (issue #6360). Normalizing the + configured path to ``/path/`` form before joining makes the behavior + consistent regardless of how the user wrote it. + + Args: + base_url: The base URL printed by the frontend dev server. + frontend_path: The configured ``Config.frontend_path``. + + Returns: + The URL with the frontend path correctly applied. + """ + if not frontend_path: + return base_url + normalized = "/" + frontend_path.strip("/") + "/" + return urljoin(base_url, normalized) + + def notify_backend(host: str | None = None): """Output a string notifying where the backend is running. @@ -226,9 +249,9 @@ def run_process_and_launch_url( match = re.search(constants.ReactRouter.FRONTEND_LISTENING_REGEX, line) if match: if first_run: - url = match.group(1) - if get_config().frontend_path != "": - url = urljoin(url, get_config().frontend_path) + url = _apply_frontend_path( + match.group(1), get_config().frontend_path + ) notify_frontend(url, backend_present) if backend_present: diff --git a/tests/units/utils/test_exec.py b/tests/units/utils/test_exec.py new file mode 100644 index 00000000000..c227c0b12c6 --- /dev/null +++ b/tests/units/utils/test_exec.py @@ -0,0 +1,43 @@ +"""Tests for reflex.utils.exec.""" + +from __future__ import annotations + +import pytest + +from reflex.utils.exec import _apply_frontend_path + + +@pytest.mark.parametrize( + ("listening_url", "frontend_path", "expected"), + [ + # Empty frontend_path is a no-op. + ("http://localhost:3001/", "", "http://localhost:3001/"), + # Vite has not yet baked the path into the URL (e.g. prod listening line). + ("http://localhost:3001/", "/app", "http://localhost:3001/app/"), + ("http://localhost:3001/", "app", "http://localhost:3001/app/"), + ("http://localhost:3001/", "app/", "http://localhost:3001/app/"), + ("http://localhost:3001/", "/app/", "http://localhost:3001/app/"), + # Vite already prints the URL with the base appended (dev server). + # Either form of frontend_path must NOT cause the path to be duplicated. + ("http://localhost:3001/noslash/", "noslash", "http://localhost:3001/noslash/"), + ( + "http://localhost:3001/noslash/", + "/noslash", + "http://localhost:3001/noslash/", + ), + # Multi-segment frontend_path. + ( + "http://localhost:3001/", + "app/v1", + "http://localhost:3001/app/v1/", + ), + ( + "http://localhost:3001/app/v1/", + "app/v1", + "http://localhost:3001/app/v1/", + ), + ], +) +def test_apply_frontend_path(listening_url: str, frontend_path: str, expected: str): + """Issue #6360: frontend_path without a leading slash must not duplicate path segments.""" + assert _apply_frontend_path(listening_url, frontend_path) == expected From aaa7d7a28d915d534862a404d8104902ed66853a Mon Sep 17 00:00:00 2001 From: ChinmayShringi Date: Mon, 4 May 2026 18:24:10 -0400 Subject: [PATCH 2/2] fix(exec): guard against frontend_path='/' producing protocol-relative URL When frontend_path is exactly '/', strip('/') yields '', so the prior normalization produced '//' which urljoin treats as a network-path reference and strips the host. Return base_url unchanged in that case. Adds regression test cases for '/' and '//'. --- reflex/utils/exec.py | 5 ++++- tests/units/utils/test_exec.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 7b62147409f..79f6eacf66d 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -178,7 +178,10 @@ def _apply_frontend_path(base_url: str, frontend_path: str) -> str: """ if not frontend_path: return base_url - normalized = "/" + frontend_path.strip("/") + "/" + stripped = frontend_path.strip("/") + if not stripped: + return base_url + normalized = "/" + stripped + "/" return urljoin(base_url, normalized) diff --git a/tests/units/utils/test_exec.py b/tests/units/utils/test_exec.py index c227c0b12c6..9c7762a683c 100644 --- a/tests/units/utils/test_exec.py +++ b/tests/units/utils/test_exec.py @@ -12,6 +12,10 @@ [ # Empty frontend_path is a no-op. ("http://localhost:3001/", "", "http://localhost:3001/"), + # Root path "/" must also be a no-op (would otherwise produce "//", a + # protocol-relative reference that strips the host). + ("http://localhost:3001/", "/", "http://localhost:3001/"), + ("http://localhost:3001/", "//", "http://localhost:3001/"), # Vite has not yet baked the path into the URL (e.g. prod listening line). ("http://localhost:3001/", "/app", "http://localhost:3001/app/"), ("http://localhost:3001/", "app", "http://localhost:3001/app/"),