proxy.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """Handle proxying frontend requests from the backend server."""
  2. from __future__ import annotations
  3. import asyncio
  4. from contextlib import asynccontextmanager
  5. from typing import Any, AsyncGenerator
  6. from urllib.parse import urlparse
  7. import aiohttp
  8. from fastapi import FastAPI
  9. from starlette.types import ASGIApp, Receive, Scope, Send
  10. from .config import get_config
  11. from .utils import console
  12. try:
  13. from asgiproxy.config import BaseURLProxyConfigMixin, ProxyConfig
  14. from asgiproxy.context import ProxyContext
  15. from asgiproxy.proxies.http import proxy_http
  16. from asgiproxy.simple_proxy import make_simple_proxy_app
  17. except ImportError:
  18. @asynccontextmanager
  19. async def proxy_middleware(*args, **kwargs) -> AsyncGenerator[None, None]:
  20. """A no-op proxy middleware for when asgiproxy is not installed.
  21. Args:
  22. *args: The positional arguments.
  23. **kwargs: The keyword arguments.
  24. Yields:
  25. None
  26. """
  27. yield
  28. else:
  29. MAX_PROXY_RETRY = 25
  30. async def proxy_http_with_retry(
  31. *,
  32. context: ProxyContext,
  33. scope: Scope,
  34. receive: Receive,
  35. send: Send,
  36. ) -> Any:
  37. """Proxy an HTTP request with retries.
  38. Args:
  39. context: The proxy context.
  40. scope: The request scope.
  41. receive: The receive channel.
  42. send: The send channel.
  43. Returns:
  44. The response from `proxy_http`.
  45. """
  46. for _attempt in range(MAX_PROXY_RETRY):
  47. try:
  48. return await proxy_http(
  49. context=context,
  50. scope=scope,
  51. receive=receive,
  52. send=send,
  53. )
  54. except aiohttp.ClientError as err: # noqa: PERF203
  55. console.debug(
  56. f"Retrying request {scope['path']} due to client error {err!r}."
  57. )
  58. await asyncio.sleep(0.3)
  59. except Exception as ex:
  60. console.debug(
  61. f"Retrying request {scope['path']} due to unhandled exception {ex!r}."
  62. )
  63. await asyncio.sleep(0.3)
  64. def _get_proxy_app_with_context(frontend_host: str) -> tuple[ProxyContext, ASGIApp]:
  65. """Get the proxy app with the given frontend host.
  66. Args:
  67. frontend_host: The frontend host to proxy requests to.
  68. Returns:
  69. The proxy context and app.
  70. """
  71. class LocalProxyConfig(BaseURLProxyConfigMixin, ProxyConfig):
  72. upstream_base_url = frontend_host
  73. rewrite_host_header = urlparse(upstream_base_url).netloc
  74. proxy_context = ProxyContext(LocalProxyConfig())
  75. proxy_app = make_simple_proxy_app(
  76. proxy_context, proxy_http_handler=proxy_http_with_retry
  77. )
  78. return proxy_context, proxy_app
  79. @asynccontextmanager
  80. async def proxy_middleware( # pyright: ignore[reportGeneralTypeIssues]
  81. app: FastAPI,
  82. ) -> AsyncGenerator[None, None]:
  83. """A middleware to proxy requests to the separate frontend server.
  84. The proxy is installed on the / endpoint of the FastAPI instance.
  85. Args:
  86. app: The FastAPI instance.
  87. Yields:
  88. None
  89. """
  90. config = get_config()
  91. backend_port = config.backend_port
  92. frontend_host = f"http://localhost:{config.frontend_port}"
  93. proxy_context, proxy_app = _get_proxy_app_with_context(frontend_host)
  94. app.mount("/", proxy_app)
  95. console.debug(
  96. f"Proxying '/' requests on port {backend_port} to {frontend_host}"
  97. )
  98. async with proxy_context:
  99. yield