From 0ef95c4a25361df67653886d9bc1f171b671bf6c Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 4 Mar 2026 15:31:52 -0800 Subject: [PATCH 1/2] fix: include frontend_path in sirv prod command for correct 404.html fallback When frontend_path is set, the build moves all static files (including 404.html) into a subdirectory, but the sirv --single flag was hardcoded to look for 404.html at the root. This broke dynamic routes in prod. Closes #5812 Co-Authored-By: Claude Opus 4.6 --- reflex/constants/installer.py | 14 ++++++++++ reflex/utils/frontend_skeleton.py | 5 +++- tests/units/test_prerequisites.py | 43 +++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 12edb911397..bfca7e6bfaf 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -108,6 +108,20 @@ class Commands(SimpleNamespace): EXPORT = "react-router build" PROD = "sirv ./build/client --single 404.html --host" + @staticmethod + def get_prod_command(frontend_path: str = "") -> str: + """Get the prod command with the correct 404.html path for the given frontend_path. + + Args: + frontend_path: The frontend path prefix (e.g. "/app"). + + Returns: + The sirv command with the correct --single fallback path. + """ + stripped = frontend_path.strip("/") + fallback = f"{stripped}/404.html" if stripped else "404.html" + return f"sirv ./build/client --single {fallback} --host" + PATH = "package.json" _react_version = _determine_react_version() diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 96ced280fe2..ada6d5cb54c 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -168,11 +168,14 @@ def _update_react_router_config(config: Config, prerender_routes: bool = False): def _compile_package_json(): + config = get_config() return templates.package_json_template( scripts={ "dev": constants.PackageJson.Commands.DEV, "export": constants.PackageJson.Commands.EXPORT, - "prod": constants.PackageJson.Commands.PROD, + "prod": constants.PackageJson.Commands.get_prod_command( + config.frontend_path + ), }, dependencies=constants.PackageJson.DEPENDENCIES, dev_dependencies=constants.PackageJson.DEV_DEPENDENCIES, diff --git a/tests/units/test_prerequisites.py b/tests/units/test_prerequisites.py index 131904e964c..cf22404bd69 100644 --- a/tests/units/test_prerequisites.py +++ b/tests/units/test_prerequisites.py @@ -6,10 +6,12 @@ from click.testing import CliRunner from reflex.config import Config +from reflex.constants.installer import PackageJson from reflex.reflex import cli from reflex.testing import chdir from reflex.utils.decorator import cached_procedure from reflex.utils.frontend_skeleton import ( + _compile_package_json, _compile_vite_config, _update_react_router_config, ) @@ -90,6 +92,47 @@ def test_initialise_vite_config(config, expected_output): assert expected_output in output +@pytest.mark.parametrize( + ("frontend_path", "expected_command"), + [ + ("", "sirv ./build/client --single 404.html --host"), + ("/", "sirv ./build/client --single 404.html --host"), + ("/app", "sirv ./build/client --single app/404.html --host"), + ("/app/", "sirv ./build/client --single app/404.html --host"), + ("app", "sirv ./build/client --single app/404.html --host"), + ( + "/deep/nested/path", + "sirv ./build/client --single deep/nested/path/404.html --host", + ), + ], +) +def test_get_prod_command(frontend_path, expected_command): + assert PackageJson.Commands.get_prod_command(frontend_path) == expected_command + + +@pytest.mark.parametrize( + ("config", "expected_prod_script"), + [ + ( + Config(app_name="test"), + "sirv ./build/client --single 404.html --host", + ), + ( + Config(app_name="test", frontend_path="/app"), + "sirv ./build/client --single app/404.html --host", + ), + ( + Config(app_name="test", frontend_path="/deep/nested"), + "sirv ./build/client --single deep/nested/404.html --host", + ), + ], +) +def test_compile_package_json_prod_command(config, expected_prod_script, monkeypatch): + monkeypatch.setattr("reflex.utils.frontend_skeleton.get_config", lambda: config) + output = _compile_package_json() + assert f'"prod": "{expected_prod_script}"' in output + + def test_cached_procedure(): call_count = 0 From b808e063260d0302fa6221c0620b70602b1c5c77 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 4 Mar 2026 16:47:33 -0800 Subject: [PATCH 2/2] Update reflex/constants/installer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- reflex/constants/installer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index bfca7e6bfaf..84e23e51b58 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -106,7 +106,6 @@ class Commands(SimpleNamespace): DEV = "react-router dev --host" EXPORT = "react-router build" - PROD = "sirv ./build/client --single 404.html --host" @staticmethod def get_prod_command(frontend_path: str = "") -> str: