Proxy backend requests on '/' to the frontend

If the optional extra `proxy` is installed, then the backend can handle all
requests by proxy unrecognized routes to the frontend nextjs server.
This commit is contained in:
Masen Furer 2024-05-13 21:29:45 -07:00
parent 1e9ccecea0
commit 7512afa949
No known key found for this signature in database
GPG Key ID: B0008AD22B3B3A95
3 changed files with 98 additions and 1 deletions

View File

@ -63,6 +63,7 @@ setuptools = ">=69.1.1,<70.0"
httpx = ">=0.25.1,<1.0" httpx = ">=0.25.1,<1.0"
twine = ">=4.0.0,<6.0" twine = ">=4.0.0,<6.0"
tomlkit = ">=0.12.4,<1.0" tomlkit = ">=0.12.4,<1.0"
asgiproxy = { version = "==0.1.1", optional = true }
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = ">=7.1.2,<8.0" pytest = ">=7.1.2,<8.0"
@ -90,6 +91,9 @@ pytest-benchmark = ">=4.0.0,<5.0"
[tool.poetry.scripts] [tool.poetry.scripts]
reflex = "reflex.reflex:cli" reflex = "reflex.reflex:cli"
[tool.poetry.extras]
proxy = ["asgiproxy"]
[build-system] [build-system]
requires = ["poetry-core>=1.5.1"] requires = ["poetry-core>=1.5.1"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

View File

@ -13,6 +13,7 @@ import os
import platform import platform
from typing import ( from typing import (
Any, Any,
AsyncGenerator,
AsyncIterator, AsyncIterator,
Callable, Callable,
Coroutine, Coroutine,
@ -94,6 +95,23 @@ def default_overlay_component() -> Component:
return Fragment.create(connection_pulser(), connection_modal()) return Fragment.create(connection_pulser(), connection_modal())
@contextlib.asynccontextmanager
async def lifespan(api: FastAPI) -> AsyncGenerator[None, None]:
"""Context manager to handle the lifespan of the app.
Args:
api: The FastAPI instance.
Yields:
None
"""
# try to set up proxying if its enabled
from .proxy import proxy_middleware
async with proxy_middleware(api):
yield
class OverlayFragment(Fragment): class OverlayFragment(Fragment):
"""Alias for Fragment, used to wrap the overlay_component.""" """Alias for Fragment, used to wrap the overlay_component."""
@ -203,7 +221,7 @@ class App(Base):
self.middleware.append(HydrateMiddleware()) self.middleware.append(HydrateMiddleware())
# Set up the API. # Set up the API.
self.api = FastAPI() self.api = FastAPI(lifespan=lifespan)
self._add_cors() self._add_cors()
self._add_default_endpoints() self._add_default_endpoints()

75
reflex/proxy.py Normal file
View File

@ -0,0 +1,75 @@
"""Handle proxying frontend requests from the backend server."""
from __future__ import annotations
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from urllib.parse import urlparse
from fastapi import FastAPI
from starlette.types import ASGIApp
from .config import get_config
from .utils import console
try:
from asgiproxy.config import BaseURLProxyConfigMixin, ProxyConfig
from asgiproxy.context import ProxyContext
from asgiproxy.simple_proxy import make_simple_proxy_app
except ImportError:
@asynccontextmanager
async def proxy_middleware(*args, **kwargs) -> AsyncGenerator[None, None]:
"""A no-op proxy middleware for when asgiproxy is not installed.
Args:
*args: The positional arguments.
**kwargs: The keyword arguments.
Yields:
None
"""
yield
else:
def _get_proxy_app_with_context(frontend_host: str) -> tuple[ProxyContext, ASGIApp]:
"""Get the proxy app with the given frontend host.
Args:
frontend_host: The frontend host to proxy requests to.
Returns:
The proxy context and app.
"""
class LocalProxyConfig(BaseURLProxyConfigMixin, ProxyConfig):
upstream_base_url = frontend_host
rewrite_host_header = urlparse(upstream_base_url).netloc
proxy_context = ProxyContext(LocalProxyConfig())
proxy_app = make_simple_proxy_app(proxy_context)
return proxy_context, proxy_app
@asynccontextmanager
async def proxy_middleware( # pyright: ignore[reportGeneralTypeIssues]
api: FastAPI,
) -> AsyncGenerator[None, None]:
"""A middleware to proxy requests to the separate frontend server.
The proxy is installed on the / endpoint of the FastAPI instance.
Args:
api: The FastAPI instance.
Yields:
None
"""
config = get_config()
backend_port = config.backend_port
frontend_host = f"http://localhost:{config.frontend_port}"
proxy_context, proxy_app = _get_proxy_app_with_context(frontend_host)
api.mount("/", proxy_app)
console.debug(
f"Proxying '/' requests on port {backend_port} to {frontend_host}"
)
async with proxy_context:
yield