
* 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.
* Update lock file
* pre-commit fu
* AppHarness: set config frontend_port and backend_port
* integration: frontend port and backend port should return the same content
with proxying enabled by default in dev mode, both frontend and backend ports
on / should return the same content.
* Retry up to 100 times when proxying to frontend
* Reduce retry attempts to 25
Fix log level passing to subprocess
* scripts/wait_for_listening_port: primarily check HTTP responses
if the port is up or not, we don't really care... the HTTP request needs to
work and not return errors
* aiohttp is an optional dep
* adapt integration.sh for --backend-only (counter integration test)
* woops
* windows WTF?
* scratching my head 🎄
* double WTF windows
* Fix remaining integration tests
120 lines
3.6 KiB
Python
120 lines
3.6 KiB
Python
"""Handle proxying frontend requests from the backend server."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from contextlib import asynccontextmanager
|
|
from typing import Any, AsyncGenerator
|
|
from urllib.parse import urlparse
|
|
|
|
from fastapi import FastAPI
|
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
|
|
from .config import get_config
|
|
from .utils import console
|
|
|
|
try:
|
|
import aiohttp
|
|
from asgiproxy.config import BaseURLProxyConfigMixin, ProxyConfig
|
|
from asgiproxy.context import ProxyContext
|
|
from asgiproxy.proxies.http import proxy_http
|
|
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:
|
|
MAX_PROXY_RETRY = 25
|
|
|
|
async def proxy_http_with_retry(
|
|
*,
|
|
context: ProxyContext,
|
|
scope: Scope,
|
|
receive: Receive,
|
|
send: Send,
|
|
) -> Any:
|
|
"""Proxy an HTTP request with retries.
|
|
|
|
Args:
|
|
context: The proxy context.
|
|
scope: The request scope.
|
|
receive: The receive channel.
|
|
send: The send channel.
|
|
|
|
Returns:
|
|
The response from `proxy_http`.
|
|
"""
|
|
for _attempt in range(MAX_PROXY_RETRY):
|
|
try:
|
|
return await proxy_http(
|
|
context=context,
|
|
scope=scope,
|
|
receive=receive,
|
|
send=send,
|
|
)
|
|
except aiohttp.ClientError as err: # noqa: PERF203
|
|
console.debug(
|
|
f"Retrying request {scope['path']} due to client error {err!r}."
|
|
)
|
|
await asyncio.sleep(0.3)
|
|
except Exception as ex:
|
|
console.debug(
|
|
f"Retrying request {scope['path']} due to unhandled exception {ex!r}."
|
|
)
|
|
await asyncio.sleep(0.3)
|
|
|
|
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, proxy_http_handler=proxy_http_with_retry
|
|
)
|
|
return proxy_context, proxy_app
|
|
|
|
@asynccontextmanager
|
|
async def proxy_middleware( # pyright: ignore[reportGeneralTypeIssues]
|
|
app: 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:
|
|
app: 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)
|
|
app.mount("/", proxy_app)
|
|
console.debug(
|
|
f"Proxying '/' requests on port {backend_port} to {frontend_host}"
|
|
)
|
|
async with proxy_context:
|
|
yield
|