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
32 changes: 29 additions & 3 deletions reflex/utils/exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,32 @@ 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
stripped = frontend_path.strip("/")
if not stripped:
return base_url
normalized = "/" + stripped + "/"
return urljoin(base_url, normalized)
Comment on lines +179 to +185
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Edge case: frontend_path = "/" produces "//"

When frontend_path is exactly "/", "/".strip("/") yields "", so normalized becomes "//". Passing "//" to urljoin treats it as a network-path (protocol-relative) reference, which strips the host and produces a broken URL like "http://" instead of "http://localhost:3001/".

A simple guard handles it:

Suggested change
if not frontend_path:
return base_url
normalized = "/" + frontend_path.strip("/") + "/"
return urljoin(base_url, normalized)
if not frontend_path:
return base_url
stripped = frontend_path.strip("/")
if not stripped:
return base_url
normalized = "/" + stripped + "/"
return urljoin(base_url, normalized)



def notify_backend(host: str | None = None):
"""Output a string notifying where the backend is running.

Expand Down Expand Up @@ -226,9 +252,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:
Expand Down
47 changes: 47 additions & 0 deletions tests/units/utils/test_exec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""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/"),
# 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/"),
("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
Loading