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
4 changes: 2 additions & 2 deletions docs/advanced_onboarding/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ The imported module must still define `app` at module level.
- `frontend_port` (default `3000`) and `backend_port` (default `8000`) control which ports the frontend and backend listen on. In dev mode, if a port is taken, the next available port is used.
- `backend_host` (default `0.0.0.0`) is the address the backend server binds to.
- `api_url` (default `http://localhost:8000`) is the URL the user's **browser** uses to reach the backend. You typically don't need to set it: when `api_url` points at localhost, the frontend substitutes the domain the app was served from, so a backend reachable at the same address as the frontend (e.g. behind a reverse proxy or load balancer) is found automatically. Set `api_url` only when the backend is listening on a different address than the frontend, e.g. `https://api.example.com`.
- `deploy_url` (default `http://localhost:3000`) is the public URL where the **frontend** is hosted. Reflex uses it wherever an absolute frontend URL is needed — most notably for the links in the generated `sitemap.xml`. It is also the origin browsers will present when connecting to the backend, which matters for CORS (below).
- `deploy_url` (unset by default) is the public URL where the **frontend** is hosted when it is served from a different origin than the backend. Reflex uses it wherever an absolute frontend URL is needed — most notably for the links in the generated `sitemap.xml`, which fall back to `api_url` when it is unset (`config.backend_url` exposes that resolved value). It is also the origin browsers will present when connecting to the backend, which matters for CORS (below).

## CORS

The backend only accepts cross-origin requests from origins listed in `cors_allowed_origins`. The default, `["*"]`, allows any origin — convenient in development, but in production restrict it to the origins your frontend is actually served from (typically the origin of `deploy_url`):
The backend only accepts cross-origin requests from origins listed in `cors_allowed_origins`. By default any origin is allowed in dev mode. In prod mode the backend serves the frontend from its own origin, so by default no cross-origin requests are allowed at all and same-origin requests pass; when `deploy_url` is set, only that origin is allowed. Set `cors_allowed_origins` explicitly when the frontend is reachable from several origins, or to `["*"]` to allow any origin in production:

```python
config = rx.Config(
Expand Down
2 changes: 1 addition & 1 deletion docs/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ If the build uses a custom `REFLEX_WEB_WORKDIR`, pass that environment variable

The `reflex-docs` integration CI jobs run the frontend tests after building the production site, using the installed React and bundler dependencies.

Public URLs use `deploy_url` and `frontend_path`. Keep the default localhost origin expected by the Helm sitemap rewrite; environments may override `REFLEX_DEPLOY_URL`. The `/docs` mount is configured separately.
Public URLs use `deploy_url` and `frontend_path`. `deploy_url` is unset by default, so the sitemap uses the backend's own origin (`config.backend_url`, i.e. `api_url`), which the Helm sitemap rewrite maps to the public domain; environments may override `REFLEX_DEPLOY_URL`. The `/docs` mount is configured separately.

The docs app serves permanent HTTP 301 redirects for its legacy URLs when the Reflex backend serves the frontend. In development or when HTML is hosted separately, requests reach the frontend instead: the redirect pages retain client navigation and prerendered HTML includes an immediate refresh, canonical link, noindex directive, and a usable destination link. That fallback navigates readers but returns HTTP 200. For HTTP 301 semantics on a separate frontend/CDN, configure redirects at that host's edge using the `redirects` list in `reflex_docs/reflex_docs.py`; backend middleware alone cannot redirect requests it never receives.

Expand Down
4 changes: 2 additions & 2 deletions docs/app/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
@pytest.mark.parametrize(
"deploy_url,expected",
[
(None, "http://localhost:3000"),
(None, None),
("https://staging.example.com", "https://staging.example.com"),
],
)
def test_docs_deployment_origin_comes_from_environment(
monkeypatch, deploy_url, expected
):
"""Builds preserve the chart's expected origin unless explicitly overridden."""
"""Builds leave deploy_url unset (same-origin backend) unless explicitly overridden."""
monkeypatch.delenv("REFLEX_DEPLOY_URL", raising=False)
monkeypatch.delenv("REFLEX_FRONTEND_PORT", raising=False)
if deploy_url is not None:
Expand Down
1 change: 1 addition & 0 deletions news/+cors-prod-default.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
In prod mode, when `deploy_url` is unset (now the default), the backend no longer sends CORS headers, so browsers cannot read cross-origin responses, and Socket.IO accepts same-origin connections only. With `deploy_url` set, only that origin is allowed. Previously any origin was allowed. Set `cors_allowed_origins` explicitly for other setups (`["*"]` restores the old behavior).
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`deploy_url` now defaults to `None` instead of `http://localhost:3000` and is no longer derived from `frontend_port`. The new `Config.backend_url` property returns `deploy_url` when set and `api_url` otherwise; the sitemap plugin builds absolute links from it.
22 changes: 13 additions & 9 deletions packages/reflex-base/src/reflex_base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ class BaseConfig:
backend_port: The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
backend_path: The path prefix for backend routes. For example, "/api" mounts the event websocket, /ping, /_upload, /_health, and /_all_routes under /api, and is automatically included in URLs baked into the frontend. Changing this requires a full `reflex run` restart — routes are registered at startup.
api_url: The backend url the frontend will connect to. Only needs to be set when the backend is listening on a different address than the frontend.
deploy_url: The url the frontend will be hosted on. Used to build absolute frontend URLs, e.g. links in the generated sitemap.xml.
deploy_url: The public url the frontend is hosted on, when it differs from the backend origin. Unset by default: in prod mode the backend then serves the frontend from its own origin, and backend_url falls back to api_url.
backend_host: The url the backend will be hosted on.
trusted_proxy_hops: Number of trusted reverse proxies used to resolve client IPs from X-Forwarded-For. Defaults to 0, which ignores X-Forwarded-For.
db_url: The database url used by rx.Model.
Expand All @@ -173,7 +173,7 @@ class BaseConfig:
bun_path: The bun path.
frozen_lockfile: Run frontend package manager in lockfile-enforcing mode (only honored by bun).
static_page_generation_timeout: Timeout to do a production build of a frontend page.
cors_allowed_origins: Comma separated list of origins that are allowed to connect to the backend API.
cors_allowed_origins: Comma separated list of origins that are allowed to connect to the backend API. The default allows any origin in dev mode; in prod mode it allows only the origin of deploy_url, or only same-origin requests when deploy_url is unset.
vite_allowed_hosts: Allowed hosts for the Vite dev server. Set to True to allow all hosts, or provide a list of hostnames (e.g. ["myservice.local"]) to allow specific ones. Prevents 403 errors in Docker, Codespaces, reverse proxies, etc.
react_strict_mode: Whether to use React strict mode.
react_compiler: Enable experimental React Compiler memoization for generated components in development and production. Defaults to False.
Expand Down Expand Up @@ -212,7 +212,7 @@ class BaseConfig:

api_url: str = f"http://localhost:{constants.DefaultPorts.BACKEND_PORT}"

deploy_url: str | None = f"http://localhost:{constants.DefaultPorts.FRONTEND_PORT}"
deploy_url: str | None = None
Comment thread
FarhanAliRaza marked this conversation as resolved.

backend_host: str = "0.0.0.0"

Expand Down Expand Up @@ -733,6 +733,16 @@ def prepend_backend_path(self, path: str) -> str:
"""
return self._prepend_path(path, self.backend_path)

@property
def backend_url(self) -> str:
"""The public url the app is reachable at.

Returns:
``deploy_url`` when set, otherwise ``api_url``, which is the origin the
backend serves the frontend from.
"""
return self.deploy_url or self.api_url

@property
def app_module(self) -> ModuleType | None:
"""The app module if `app_module_import` is set.
Expand Down Expand Up @@ -818,12 +828,6 @@ def _replace_defaults(self, **kwargs):
if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
self.api_url = f"http://localhost:{kwargs['backend_port']}"

if (
"deploy_url" not in self._non_default_attributes
and "frontend_port" in kwargs
):
self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"

if "api_url" not in self._non_default_attributes:
# If running in Github Codespaces, override API_URL
codespace_name = os.getenv("CODESPACE_NAME")
Expand Down
2 changes: 1 addition & 1 deletion packages/reflex-base/src/reflex_base/plugins/sitemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def generate_links_for_sitemap(
from reflex_base.config import get_config

app_config = get_config()
deploy_url = app_config.deploy_url
deploy_url = app_config.backend_url
Comment thread
FarhanAliRaza marked this conversation as resolved.

links: list[SitemapLink] = []

Expand Down
45 changes: 37 additions & 8 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,38 @@ def _is_location_specifier(specifier: str) -> bool:
)


_DEFAULT_PORTS = {"http": 80, "https": 443}


def _cors_allowed_origins() -> list[str] | None:
"""Resolve the origins the backend accepts cross-origin requests from.

An explicitly configured ``cors_allowed_origins`` is used as is. The
``["*"]`` default stays open in dev mode, but in prod mode it narrows to
the origin of ``deploy_url`` so only the configured frontend can reach
the backend. Without a ``deploy_url`` the backend serves the frontend
itself, so CORS is left unconfigured and only same-origin requests pass.

Returns:
The allowed origins, where ``["*"]`` means any origin, or None to
leave CORS unconfigured.
"""
config = get_config()
if "cors_allowed_origins" in config._non_default_attributes or not is_prod_mode():
return list(config.cors_allowed_origins)
if not config.deploy_url:
return None
# Match the browser's Origin serialization: lowercase host, no default port.
parts = urllib.parse.urlsplit(config.deploy_url)
host = parts.hostname or ""
if ":" in host:
host = f"[{host}]"
port = parts.port
if port is None or port == _DEFAULT_PORTS.get(parts.scheme):
return [f"{parts.scheme}://{host}"]
return [f"{parts.scheme}://{host}:{port}"]


@dataclasses.dataclass()
class App(MiddlewareMixin, LifespanMixin):
"""The main Reflex app that encapsulates the backend and frontend.
Expand Down Expand Up @@ -678,13 +710,7 @@ def _setup_state(self) -> None:
self.sio = AsyncServer(
async_mode="asgi",
cors_allowed_origins=(
(
"*"
if config.cors_allowed_origins == ("*",)
else list(config.cors_allowed_origins)
)
if config.transport == "websocket"
else []
_cors_allowed_origins() if config.transport == "websocket" else []
),
cors_credentials=config.transport == "websocket",
max_http_buffer_size=environment.REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE.get(),
Expand Down Expand Up @@ -982,12 +1008,15 @@ def _add_cors(api: Starlette):
Args:
api: The Starlette app to add CORS middleware to.
"""
allow_origins = _cors_allowed_origins()
if allow_origins is None:
return
api.add_middleware(
cors.CORSMiddleware,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_origins=get_config().cors_allowed_origins,
allow_origins=allow_origins,
)

@property
Expand Down
30 changes: 15 additions & 15 deletions tests/units/plugins/test_sitemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def test_generate_links_for_sitemap_static_routes(
caplog: Pytest log capture fixture.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "https://example.com"
mock_get_config.return_value.backend_url = "https://example.com"

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -160,7 +160,7 @@ def test_generate_links_for_sitemap_dynamic_routes(
caplog: Pytest log capture fixture.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "https://sub.example.org"
mock_get_config.return_value.backend_url = "https://sub.example.org"
now = datetime.datetime(2023, 6, 13, 12, 0, 0)

def mock_component():
Expand Down Expand Up @@ -231,7 +231,7 @@ def test_generate_links_for_sitemap_404_route(
caplog: Pytest log capture fixture.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = None # No deploy URL
mock_get_config.return_value.backend_url = None # No deploy URL

def mock_component():
return rx.text("404")
Expand Down Expand Up @@ -277,7 +277,7 @@ def test_generate_links_for_sitemap_opt_out(mock_get_config: MagicMock):
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = None # No deploy URL
mock_get_config.return_value.backend_url = None # No deploy URL

def mock_component():
return rx.text("Unlisted")
Expand Down Expand Up @@ -317,7 +317,7 @@ def test_generate_links_for_sitemap_loc_override(mock_get_config: MagicMock):
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "http://localhost:3000"
mock_get_config.return_value.backend_url = "http://localhost:3000"

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -358,7 +358,7 @@ def test_generate_links_for_sitemap_priority_clamping(mock_get_config: MagicMock
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "https://example.com"
mock_get_config.return_value.backend_url = "https://example.com"

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -413,7 +413,7 @@ def test_generate_links_for_sitemap_no_deploy_url(mock_get_config: MagicMock):
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = None
mock_get_config.return_value.backend_url = None
Comment thread
FarhanAliRaza marked this conversation as resolved.

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -467,7 +467,7 @@ def test_generate_links_for_sitemap_deploy_url_trailing_slash(
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "https://example.com/"
mock_get_config.return_value.backend_url = "https://example.com/"

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -497,7 +497,7 @@ def test_generate_links_for_sitemap_loc_leading_slash(mock_get_config: MagicMock
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "https://example.com"
mock_get_config.return_value.backend_url = "https://example.com"

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -527,7 +527,7 @@ def test_generate_links_for_sitemap_loc_full_url(mock_get_config: MagicMock):
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "https://example.com"
mock_get_config.return_value.backend_url = "https://example.com"

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -557,7 +557,7 @@ def test_generate_links_trailing_slash_always(mock_get_config: MagicMock):
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "https://example.com"
mock_get_config.return_value.backend_url = "https://example.com"

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -609,7 +609,7 @@ def test_generate_links_trailing_slash_never(mock_get_config: MagicMock):
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "https://example.com"
mock_get_config.return_value.backend_url = "https://example.com"

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -664,7 +664,7 @@ def test_generate_links_trailing_slash_never_no_deploy_url_index(
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = None
mock_get_config.return_value.backend_url = None

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -706,7 +706,7 @@ def test_generate_links_trailing_slash_preserve(mock_get_config: MagicMock):
mock_get_config: Mock for the get_config function.
"""
mock_get_config.return_value.frontend_path = ""
mock_get_config.return_value.deploy_url = "https://example.com"
mock_get_config.return_value.backend_url = "https://example.com"

def mock_component():
return rx.text("Test")
Expand Down Expand Up @@ -747,7 +747,7 @@ def test_default_sitemap_locations_include_frontend_path(
mock_get_config, frontend_path
):
"""Mounted apps must advertise their public home and article URLs."""
mock_get_config.return_value.deploy_url = "https://example.com"
mock_get_config.return_value.backend_url = "https://example.com"
mock_get_config.return_value.frontend_path = frontend_path
pages = [MagicMock(route=route, context={}) for route in ("index", "intro")]
links = generate_links_for_sitemap(pages, trailing_slash="always")
Expand Down
Loading
Loading