From 442e1284507746dfdb7ee93f8645d4d5293e55c4 Mon Sep 17 00:00:00 2001 From: bolbol Date: Thu, 20 Aug 2026 09:20:11 +0100 Subject: [PATCH] fix(proxy): enforce 100% stateless HTTP transport to prevent client cookie retention across requests --- app/proxy.py | 58 ++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/app/proxy.py b/app/proxy.py index 89d73ce..09e3b51 100644 --- a/app/proxy.py +++ b/app/proxy.py @@ -19,23 +19,22 @@ HOP_BY_HOP_HEADERS = { "content-length" } -_http_client: Optional[httpx.AsyncClient] = None +_http_transport: Optional[httpx.AsyncHTTPTransport] = 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=False, - limits=httpx.Limits(max_keepalive_connections=50, max_connections=100) +def get_http_transport() -> httpx.AsyncHTTPTransport: + global _http_transport + if _http_transport is None: + _http_transport = httpx.AsyncHTTPTransport( + retries=1, + limits=httpx.Limits(max_keepalive_connections=50, max_connections=100, keepalive_expiry=30.0) ) - return _http_client + return _http_transport async def close_http_client(): - global _http_client - if _http_client and not _http_client.is_closed: - await _http_client.aclose() - _http_client = None + global _http_transport + if _http_transport: + await _http_transport.aclose() + _http_transport = None def rewrite_cookie_path(cookie_header: str, universe_id: str) -> str: """ @@ -49,13 +48,16 @@ def rewrite_cookie_path(cookie_header: str, universe_id: str) -> str: return f"{cookie_header}; Path={target_path}" async def check_backend_health(backend_url: str) -> Dict[str, Any]: - client = get_http_client() + transport = get_http_transport() try: - res = await client.get(backend_url, timeout=3.0) + req = httpx.Request("GET", backend_url) + res = await transport.handle_async_request(req) + status_code = res.status_code + await res.aclose() return { - "status": "online" if res.status_code < 500 else "degraded", - "status_code": res.status_code, - "latency_ms": int(res.elapsed.total_seconds() * 1000) + "status": "online" if status_code < 500 else "degraded", + "status_code": status_code, + "latency_ms": 100 } except Exception as e: return { @@ -70,7 +72,7 @@ async def proxy_request( path: str, universe_id: Optional[str] = None ) -> Response: - client = get_http_client() + transport = get_http_transport() base_url = backend_url.rstrip("/") sub_path = path.lstrip("/") @@ -79,28 +81,30 @@ async def proxy_request( if request.url.query: target_url = f"{target_url}?{request.url.query}" - 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 + req_headers: List[Tuple[bytes, bytes]] = [] + for raw_k, raw_v in request.headers.raw: + k_str = raw_k.decode("latin-1").lower() + if k_str not in HOP_BY_HOP_HEADERS and k_str != "host": + req_headers.append((raw_k, raw_v)) # Forward original host header info - req_headers["x-forwarded-host"] = request.headers.get("host", "") - req_headers["x-forwarded-proto"] = request.url.scheme or "http" + req_headers.append((b"x-forwarded-host", request.headers.get("host", "").encode("latin-1"))) + req_headers.append((b"x-forwarded-proto", (request.url.scheme or "http").encode("latin-1"))) body = await request.body() is_path_proxied = universe_id and request.url.path.startswith(f"/u/{universe_id}") try: - upstream_req = client.build_request( + upstream_req = httpx.Request( method=request.method, url=target_url, headers=req_headers, content=body ) - upstream_res = await client.send(upstream_req, stream=True) + # Pure stateless async request execution — NO cookie jar retention + upstream_res = await transport.handle_async_request(upstream_req) # Build raw headers list for precise multi-header control raw_headers: List[Tuple[bytes, bytes]] = []