Bläddra i källkod

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.
Masen Furer 1 år sedan
förälder
incheckning
7512afa949
3 ändrade filer med 98 tillägg och 1 borttagningar
  1. 4 0
      pyproject.toml
  2. 19 1
      reflex/app.py
  3. 75 0
      reflex/proxy.py

+ 4 - 0
pyproject.toml

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

+ 19 - 1
reflex/app.py

@@ -13,6 +13,7 @@ import os
 import platform
 from typing import (
     Any,
+    AsyncGenerator,
     AsyncIterator,
     Callable,
     Coroutine,
@@ -94,6 +95,23 @@ def default_overlay_component() -> Component:
     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):
     """Alias for Fragment, used to wrap the overlay_component."""
 
@@ -203,7 +221,7 @@ class App(Base):
         self.middleware.append(HydrateMiddleware())
 
         # Set up the API.
-        self.api = FastAPI()
+        self.api = FastAPI(lifespan=lifespan)
         self._add_cors()
         self._add_default_endpoints()
 

+ 75 - 0
reflex/proxy.py

@@ -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