feat: initialisation Hermes Hub — interface multi-univers securisee (FastAPI, proxy async, design tokens)
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
import httpx
|
||||
from typing import AsyncGenerator, Dict, Any, Optional
|
||||
from fastapi import Request, Response
|
||||
from fastapi.responses import StreamingResponse
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("hermes_hub.proxy")
|
||||
|
||||
HOP_BY_HOP_HEADERS = {
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailers",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
"content-length"
|
||||
}
|
||||
|
||||
# Global async client for connection pooling
|
||||
_http_client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
def get_http_client() -> httpx.AsyncClient:
|
||||
global _http_client
|
||||
if _http_client is None or _http_client.is_closed:
|
||||
_http_client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(connect=5.0, read=120.0, write=60.0, pool=30.0),
|
||||
follow_redirects=True,
|
||||
limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)
|
||||
)
|
||||
return _http_client
|
||||
|
||||
async def close_http_client():
|
||||
global _http_client
|
||||
if _http_client and not _http_client.is_closed:
|
||||
await _http_client.aclose()
|
||||
_http_client = None
|
||||
|
||||
async def check_backend_health(backend_url: str) -> Dict[str, Any]:
|
||||
client = get_http_client()
|
||||
try:
|
||||
# Test root or /health
|
||||
res = await client.get(backend_url, timeout=3.0)
|
||||
return {
|
||||
"status": "online" if res.status_code < 500 else "degraded",
|
||||
"status_code": res.status_code,
|
||||
"latency_ms": int(res.elapsed.total_seconds() * 1000)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "offline",
|
||||
"error": str(e),
|
||||
"latency_ms": None
|
||||
}
|
||||
|
||||
async def proxy_request(
|
||||
request: Request,
|
||||
backend_url: str,
|
||||
path: str
|
||||
) -> Response:
|
||||
client = get_http_client()
|
||||
|
||||
# Strip trailing slash from backend_url and leading from path
|
||||
base_url = backend_url.rstrip("/")
|
||||
sub_path = path.lstrip("/")
|
||||
target_url = f"{base_url}/{sub_path}" if sub_path else base_url
|
||||
|
||||
if request.url.query:
|
||||
target_url = f"{target_url}?{request.url.query}"
|
||||
|
||||
# Filter incoming request headers
|
||||
req_headers = {}
|
||||
for key, value in request.headers.items():
|
||||
if key.lower() not in HOP_BY_HOP_HEADERS and key.lower() != "host":
|
||||
req_headers[key] = value
|
||||
|
||||
body = await request.body()
|
||||
|
||||
try:
|
||||
upstream_req = client.build_request(
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
headers=req_headers,
|
||||
content=body
|
||||
)
|
||||
|
||||
upstream_res = await client.send(upstream_req, stream=True)
|
||||
|
||||
# Filter response headers
|
||||
res_headers = {}
|
||||
for key, value in upstream_res.headers.items():
|
||||
if key.lower() not in HOP_BY_HOP_HEADERS:
|
||||
res_headers[key] = value
|
||||
|
||||
async def stream_content() -> AsyncGenerator[bytes, None]:
|
||||
try:
|
||||
async for chunk in upstream_res.aiter_raw():
|
||||
yield chunk
|
||||
finally:
|
||||
await upstream_res.aclose()
|
||||
|
||||
return StreamingResponse(
|
||||
stream_content(),
|
||||
status_code=upstream_res.status_code,
|
||||
headers=res_headers,
|
||||
media_type=upstream_res.headers.get("content-type")
|
||||
)
|
||||
except httpx.ConnectError:
|
||||
logger.error(f"Failed to connect to backend at {target_url}")
|
||||
return Response(
|
||||
content=f"<html><head><title>Backend Unavailable</title></head><body style='background:#121212;color:#ef4444;font-family:sans-serif;padding:2rem;'><h2>Instance Hermes inaccessible</h2><p>Le backend sur <code>{backend_url}</code> ne répond pas. Vérifiez le tunnel Tailscale ou l'état du conteneur.</p></body></html>",
|
||||
status_code=502,
|
||||
media_type="text/html"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Proxy error for {target_url}: {e}")
|
||||
return Response(
|
||||
content=f"Proxy Error: {str(e)}",
|
||||
status_code=500,
|
||||
media_type="text/plain"
|
||||
)
|
||||
Reference in New Issue
Block a user