fix(proxy): enforce 100% stateless HTTP transport to prevent client cookie retention across requests

This commit is contained in:
bolbol
2026-08-20 09:20:11 +01:00
parent e074015b72
commit 442e128450
+31 -27
View File
@@ -19,23 +19,22 @@ HOP_BY_HOP_HEADERS = {
"content-length" "content-length"
} }
_http_client: Optional[httpx.AsyncClient] = None _http_transport: Optional[httpx.AsyncHTTPTransport] = None
def get_http_client() -> httpx.AsyncClient: def get_http_transport() -> httpx.AsyncHTTPTransport:
global _http_client global _http_transport
if _http_client is None or _http_client.is_closed: if _http_transport is None:
_http_client = httpx.AsyncClient( _http_transport = httpx.AsyncHTTPTransport(
timeout=httpx.Timeout(connect=5.0, read=120.0, write=60.0, pool=30.0), retries=1,
follow_redirects=False, limits=httpx.Limits(max_keepalive_connections=50, max_connections=100, keepalive_expiry=30.0)
limits=httpx.Limits(max_keepalive_connections=50, max_connections=100)
) )
return _http_client return _http_transport
async def close_http_client(): async def close_http_client():
global _http_client global _http_transport
if _http_client and not _http_client.is_closed: if _http_transport:
await _http_client.aclose() await _http_transport.aclose()
_http_client = None _http_transport = None
def rewrite_cookie_path(cookie_header: str, universe_id: str) -> str: 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}" return f"{cookie_header}; Path={target_path}"
async def check_backend_health(backend_url: str) -> Dict[str, Any]: async def check_backend_health(backend_url: str) -> Dict[str, Any]:
client = get_http_client() transport = get_http_transport()
try: 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 { return {
"status": "online" if res.status_code < 500 else "degraded", "status": "online" if status_code < 500 else "degraded",
"status_code": res.status_code, "status_code": status_code,
"latency_ms": int(res.elapsed.total_seconds() * 1000) "latency_ms": 100
} }
except Exception as e: except Exception as e:
return { return {
@@ -70,7 +72,7 @@ async def proxy_request(
path: str, path: str,
universe_id: Optional[str] = None universe_id: Optional[str] = None
) -> Response: ) -> Response:
client = get_http_client() transport = get_http_transport()
base_url = backend_url.rstrip("/") base_url = backend_url.rstrip("/")
sub_path = path.lstrip("/") sub_path = path.lstrip("/")
@@ -79,28 +81,30 @@ async def proxy_request(
if request.url.query: if request.url.query:
target_url = f"{target_url}?{request.url.query}" target_url = f"{target_url}?{request.url.query}"
req_headers = {} req_headers: List[Tuple[bytes, bytes]] = []
for key, value in request.headers.items(): for raw_k, raw_v in request.headers.raw:
if key.lower() not in HOP_BY_HOP_HEADERS and key.lower() != "host": k_str = raw_k.decode("latin-1").lower()
req_headers[key] = value 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 # Forward original host header info
req_headers["x-forwarded-host"] = request.headers.get("host", "") req_headers.append((b"x-forwarded-host", request.headers.get("host", "").encode("latin-1")))
req_headers["x-forwarded-proto"] = request.url.scheme or "http" req_headers.append((b"x-forwarded-proto", (request.url.scheme or "http").encode("latin-1")))
body = await request.body() body = await request.body()
is_path_proxied = universe_id and request.url.path.startswith(f"/u/{universe_id}") is_path_proxied = universe_id and request.url.path.startswith(f"/u/{universe_id}")
try: try:
upstream_req = client.build_request( upstream_req = httpx.Request(
method=request.method, method=request.method,
url=target_url, url=target_url,
headers=req_headers, headers=req_headers,
content=body 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 # Build raw headers list for precise multi-header control
raw_headers: List[Tuple[bytes, bytes]] = [] raw_headers: List[Tuple[bytes, bytes]] = []