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
11 changes: 10 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,22 @@ Apps as factory functions, run via `AppHarness`:
```python
def SomeApp():
import reflex as rx

class State(rx.State):
value: str = ""

def index():
return rx.box(rx.text(State.value))

app = rx.App()
app.add_page(index)


@pytest.fixture(scope="module")
def some_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
with AppHarness.create(root=tmp_path_factory.mktemp("some_app"), app_source=SomeApp) as harness:
with AppHarness.create(
root=tmp_path_factory.mktemp("some_app"), app_source=SomeApp
) as harness:
yield harness
```

Expand All @@ -88,6 +94,7 @@ Reflex has downstream users — don't break them. Provide a fallback path during
**Runtime warning** via `console.deprecate()`:
```python
from reflex_base.utils import console

console.deprecate(
feature_name="OldFeature",
reason="Use NewFeature instead.",
Expand All @@ -101,8 +108,10 @@ Set `deprecation_version` to the next dot version of the latest tag (`git fetch
```python
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing_extensions import deprecated

@deprecated("Use new_method() instead")
def old_method(self) -> str: ...
```
Expand Down
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def index():
"Generate Image",
on_click=State.get_image,
width="25em",
loading=State.processing
loading=State.processing,
),
rx.cond(
State.complete,
Expand All @@ -175,6 +175,7 @@ def index():
height="100vh",
)


# Add state and page to the app.
app = rx.App()
app.add_page(index, title="Reflex:DALL-E")
Expand All @@ -192,9 +193,7 @@ Let's start with the UI.

```python
def index():
return rx.center(
...
)
return rx.center(...)
```

This `index` function defines the frontend of the app.
Expand All @@ -211,11 +210,11 @@ Reflex represents your UI as a function of your state.
```python
class State(rx.State):
"""The app state."""

prompt = ""
image_url = ""
processing = False
complete = False

```

The state defines all the variables (called vars) in an app that can change and the functions that change them.
Expand All @@ -232,9 +231,7 @@ def get_image(self):

self.processing, self.complete = True, False
yield
response = openai_client.images.generate(
prompt=self.prompt, n=1, size="1024x1024"
)
response = openai_client.images.generate(prompt=self.prompt, n=1, size="1024x1024")
self.image_url = response.data[0].url
self.processing, self.complete = False, True
```
Expand Down
24 changes: 24 additions & 0 deletions docs/hosting/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,30 @@ config = rx.Config(
It is also possible to set the environment variable `API_URL` at run time or
export time to retain the default for local development.

## Proxying to a Subpath

If you want to serve the backend behind a reverse proxy at a subpath (e.g.
nginx routing `/api/*` to Reflex), set `backend_path` on the config instead of
baking the prefix into `api_url`. Every backend endpoint (event websocket,
`/ping`, `/_upload`, `/_health`, `/_all_routes`) is mounted under that prefix,
and the frontend baked into the export automatically calls the backend at the
prefixed URLs — no request rewriting in the proxy is required.

```python
config = rx.Config(
app_name="your_app_name",
api_url="http://app.example.com:8000",
backend_path="/api",
)
```

`frontend_path` plays the analogous role for the frontend and the two are
independent.

Note: changing `backend_path` (or `frontend_path`) requires a full restart of
`reflex run` — routes and mount points are registered at startup, so hot
reload alone will not move them.

## Production Mode

Then run your app in production mode:
Expand Down
33 changes: 30 additions & 3 deletions packages/reflex-base/src/reflex_base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ class BaseConfig:
frontend_port: The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
frontend_path: The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app
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. This must be updated if the backend is hosted elsewhere, or in production.
deploy_url: The url the frontend will be hosted on.
backend_host: The url the backend will be hosted on.
Expand Down Expand Up @@ -194,6 +195,8 @@ class BaseConfig:

backend_port: int | None = None

backend_path: str = ""

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

deploy_url: str | None = f"http://localhost:{constants.DefaultPorts.FRONTEND_PORT}"
Expand Down Expand Up @@ -476,6 +479,21 @@ def json(self) -> str:

return json.dumps(self, default=serialize)

@staticmethod
def _prepend_path(path: str, prefix: str) -> str:
"""Prepend ``prefix`` (normalized to ``/prefix``) to ``path`` when both are non-empty.

Args:
path: The path to prepend the prefix to.
prefix: The configured prefix (e.g. ``frontend_path`` or ``backend_path``).

Returns:
The path with the prefix prepended if it begins with a slash, otherwise the original path.
"""
if prefix and path.startswith("/"):
return f"/{prefix.strip('/')}{path}"
return path

def prepend_frontend_path(self, path: str) -> str:
"""Prepend the frontend path to a given path.

Expand All @@ -485,9 +503,18 @@ def prepend_frontend_path(self, path: str) -> str:
Returns:
The path with the frontend path prepended if it begins with a slash, otherwise the original path.
"""
if self.frontend_path and path.startswith("/"):
return f"/{self.frontend_path.strip('/')}{path}"
return path
return self._prepend_path(path, self.frontend_path)

def prepend_backend_path(self, path: str) -> str:
"""Prepend the backend path to a given path.

Args:
path: The path to prepend the backend path to.

Returns:
The path with the backend path prepended if it begins with a slash, otherwise the original path.
"""
return self._prepend_path(path, self.backend_path)

@property
def app_module(self) -> ModuleType | None:
Expand Down
2 changes: 1 addition & 1 deletion packages/reflex-base/src/reflex_base/constants/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def get_url(self) -> str:

# Get the API URL from the config.
config = get_config()
url = "".join([config.api_url, str(self)])
url = "".join([config.api_url, config.prepend_backend_path(str(self))])

# The event endpoint is a websocket.
if self == Endpoint.EVENT:
Expand Down
22 changes: 15 additions & 7 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,10 @@ async def modified_send(message: Message):
return await self.app(scope, receive, modified_send)

socket_app_with_headers = HeaderMiddleware(socket_app)
self._api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
self._api.mount(
config.prepend_backend_path(str(constants.Endpoint.EVENT)),
socket_app_with_headers,
)

# Check the exception handlers
self._validate_exception_handlers()
Expand Down Expand Up @@ -683,13 +686,14 @@ def _add_default_endpoints(self):
if not self._api:
return

config = get_config()
self._api.add_route(
str(constants.Endpoint.PING),
config.prepend_backend_path(str(constants.Endpoint.PING)),
ping,
methods=["GET"],
)
self._api.add_route(
str(constants.Endpoint.HEALTH),
config.prepend_backend_path(str(constants.Endpoint.HEALTH)),
health,
methods=["GET"],
)
Expand All @@ -700,20 +704,21 @@ def _add_optional_endpoints(self):

if not self._api:
return
config = get_config()
upload_is_used_marker = (
prerequisites.get_backend_dir() / constants.Dirs.UPLOAD_IS_USED
)
if Upload.is_used or upload_is_used_marker.exists():
# To upload files.
self._api.add_route(
str(constants.Endpoint.UPLOAD),
config.prepend_backend_path(str(constants.Endpoint.UPLOAD)),
upload(self),
methods=["POST"],
)

# To access uploaded files.
self._api.mount(
str(constants.Endpoint.UPLOAD),
config.prepend_backend_path(str(constants.Endpoint.UPLOAD)),
UploadedFilesHeadersMiddleware(StaticFiles(directory=get_upload_dir())),
name="uploaded_files",
)
Expand All @@ -722,7 +727,7 @@ def _add_optional_endpoints(self):
upload_is_used_marker.touch()
if codespaces.is_running_in_codespaces():
self._api.add_route(
str(constants.Endpoint.AUTH_CODESPACE),
config.prepend_backend_path(str(constants.Endpoint.AUTH_CODESPACE)),
codespaces.auth_codespace,
methods=["GET"],
)
Expand Down Expand Up @@ -1558,8 +1563,11 @@ def add_all_routes_endpoint(self):
def all_routes(_request: Request) -> Response:
return JSONResponse(list(self._unevaluated_pages.keys()))

config = get_config()
self._api.add_route(
str(constants.Endpoint.ALL_ROUTES), all_routes, methods=["GET"]
config.prepend_backend_path(str(constants.Endpoint.ALL_ROUTES)),
all_routes,
Comment thread
FarhanAliRaza marked this conversation as resolved.
methods=["GET"],
)

@overload
Expand Down
3 changes: 1 addition & 2 deletions tests/integration/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,6 @@ def test_uploaded_file_security_headers(
expected_mime_type: expected Content-Type mime type.
"""
import httpx
from reflex_base.config import get_config

assert upload_file.app_instance is not None
poll_for_token(driver, upload_file)
Expand All @@ -809,7 +808,7 @@ def test_uploaded_file_security_headers(
assert upload_file.poll_for_value(upload_done, exp_not_equal="false") == "true"

# Fetch the uploaded file directly via httpx and check security headers.
upload_url = f"{get_config().api_url}/{Endpoint.UPLOAD.value}/{exp_name}"
upload_url = f"{Endpoint.UPLOAD.get_url()}/{exp_name}"
resp = httpx.get(upload_url)
assert resp.status_code == 200
assert resp.text == exp_contents
Expand Down
Loading
Loading