Skip to content
Merged
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 `http://localhost:<frontend_port>` when it is unset (`config.frontend_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 `config.frontend_url`, the default localhost origin expected by the Helm sitemap rewrite; 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
2 changes: 1 addition & 1 deletion docs/app/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_docs_deployment_origin_comes_from_environment(
if deploy_url is not None:
monkeypatch.setenv("REFLEX_DEPLOY_URL", deploy_url)
config = runpy.run_path(str(Path(__file__).parents[1] / "rxconfig.py"))["config"]
assert config.deploy_url == expected
assert config.frontend_url == expected


def test_docs_head_identifies_cross_origin_video_embeds(monkeypatch):
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.frontend_url` property returns `deploy_url` when set and `http://localhost:<frontend_port>` otherwise; the sitemap plugin builds absolute links from it.
24 changes: 15 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 frontend_url falls back to http://localhost:<frontend_port>.
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,18 @@ def prepend_backend_path(self, path: str) -> str:
"""
return self._prepend_path(path, self.backend_path)

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

Returns:
``deploy_url`` when set, otherwise ``http://localhost:<frontend_port>``.
"""
return (
self.deploy_url
or f"http://localhost:{self.frontend_port or constants.DefaultPorts.FRONTEND_PORT}"
)

@property
def app_module(self) -> ModuleType | None:
"""The app module if `app_module_import` is set.
Expand Down Expand Up @@ -818,12 +830,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.frontend_url

links: list[SitemapLink] = []

Expand Down
62 changes: 50 additions & 12 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,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 @@ -717,13 +749,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 @@ -966,10 +992,15 @@ def __call__(self) -> ASGIApp:
if asgi_app is not api:
lifespan_apps.append(asgi_app)

cors_options = App._cors_options()
top_asgi_app = _RootMiddleware(
# Covers the routes a callable transformer adds and the 500 responses
# that the outermost Starlette app's error handler sends.
cors.CORSMiddleware(asgi_app, **App._cors_options()),
(
cors.CORSMiddleware(asgi_app, **cors_options)
if cors_options is not None
else asgi_app
),
reflex_app=self,
lifespan=functools.partial(self._run_lifespan, api, lifespan_apps),
)
Expand Down Expand Up @@ -1032,17 +1063,21 @@ def _add_optional_endpoints(self):
self.add_all_routes_endpoint()

@staticmethod
def _cors_options() -> dict[str, Any]:
def _cors_options() -> dict[str, Any] | None:
"""Get the options of the CORS middleware.

Returns:
The keyword arguments for ``CORSMiddleware``.
The keyword arguments for ``CORSMiddleware``, or None when CORS is
left unconfigured so only same-origin requests are served.
"""
allow_origins = _cors_allowed_origins()
if allow_origins is None:
return None
return {
"allow_credentials": True,
"allow_methods": ["*"],
"allow_headers": ["*"],
"allow_origins": get_config().cors_allowed_origins,
"allow_origins": allow_origins,
}

@staticmethod
Expand All @@ -1052,7 +1087,10 @@ def _add_cors(api: Starlette):
Args:
api: The Starlette app to add CORS middleware to.
"""
api.add_middleware(cors.CORSMiddleware, **App._cors_options())
options = App._cors_options()
if options is None:
return
api.add_middleware(cors.CORSMiddleware, **options)

@property
def state_manager(self) -> StateManager:
Expand Down
Loading
Loading