""" Proxy all /api/documents/* requests to doc-service:8001/documents/*. Uses a module-level AsyncClient for connection pooling. Strips hop-by-hop headers that must not be forwarded. """ import os import httpx from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import Response from app.deps import get_current_user from app.models.user import User DOC_SERVICE_URL = os.environ.get("DOC_SERVICE_URL", "http://doc-service:8001") _client = httpx.AsyncClient(base_url=DOC_SERVICE_URL, timeout=120.0) router = APIRouter() # Headers that must not be forwarded in either direction. # Also strip accept-encoding so doc-service never compresses responses — # httpx decompresses transparently but the content-encoding header would # then mismatch the already-decompressed body we forward to the browser. _HOP_BY_HOP = frozenset([ "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade", "host", "accept-encoding", ]) # Additional response headers we let FastAPI recalculate rather than forward. # content-length is set automatically from the response body size. # content-type is set via the media_type argument, so strip it from headers # to avoid duplicates. _STRIP_RESPONSE = frozenset([*_HOP_BY_HOP, "content-length", "content-type"]) def _forward_headers(request: Request, user_id: str) -> dict: headers = { k: v for k, v in request.headers.items() if k.lower() not in _HOP_BY_HOP } headers["x-user-id"] = user_id return headers @router.api_route("", methods=["GET", "POST"]) @router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) async def proxy_documents( request: Request, current_user: User = Depends(get_current_user), path: str = "", ) -> Response: url = f"/documents/{path}" if path else "/documents" headers = _forward_headers(request, str(current_user.id)) body = await request.body() try: response = await _client.request( method=request.method, url=url, headers=headers, content=body, params=dict(request.query_params), ) except httpx.RequestError as exc: raise HTTPException(status_code=502, detail=f"doc-service unreachable: {exc}") resp_headers = { k: v for k, v in response.headers.items() if k.lower() not in _STRIP_RESPONSE } return Response( content=response.content, status_code=response.status_code, headers=resp_headers, media_type=response.headers.get("content-type"), )