fix(proxy): enforce 100% stateless HTTP transport to prevent client cookie retention across requests
This commit is contained in:
+31
-27
@@ -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]] = []
|
||||||
|
|||||||
Reference in New Issue
Block a user