open-navigator / api /routes /documents.py
jcbowyer's picture
Clean HuggingFace deployment without binary files
e59d91d
Raw
History Blame Contribute Delete
3.22 kB
"""
PDF document proxy for inline rendering.
The frontend renders meeting agenda/minutes PDFs inline (react-pdf), which fetches
the file bytes client-side. Government document hosts (Legistar, Granicus, SuiteOne)
rarely send CORS headers, so a direct browser fetch fails. This endpoint proxies the
bytes through the API so, to the browser, the document is same-origin.
SSRF guard: only URLs that already exist in public.event_meeting_document.document_url
are proxied — the client cannot make the server fetch an arbitrary URL.
"""
from __future__ import annotations
import httpx
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import Response
from loguru import logger
from opentelemetry import trace
from api.routes.search_postgres import get_db_pool
router = APIRouter(prefix="/document", tags=["document"])
tracer = trace.get_tracer(__name__)
# Upper bound on a proxied document. Meeting agenda/minutes PDFs are well under this;
# the cap stops a stray large upstream file from ballooning memory.
_MAX_BYTES = 50 * 1024 * 1024
# SSRF allow-list: a URL is proxyable only if it is a document we already serve.
_URL_KNOWN_SQL = """
SELECT 1
FROM event_meeting_document
WHERE document_url = $1
LIMIT 1
"""
@router.get("/proxy")
async def proxy_document(
url: str = Query(..., description="Document URL to proxy (must be a known meeting document)"),
):
"""Stream a known meeting document through the API for same-origin rendering."""
pool = await get_db_pool()
async with pool.acquire() as conn:
known = await conn.fetchval(_URL_KNOWN_SQL, url)
if not known:
# Not a document we serve — refuse rather than fetch an arbitrary URL.
raise HTTPException(status_code=404, detail="Unknown document")
with tracer.start_as_current_span("document.proxy") as span:
span.set_attribute("document.url", url)
try:
async with httpx.AsyncClient(follow_redirects=True, timeout=20.0) as client:
resp = await client.get(url, headers={"User-Agent": "OpenNavigator/1.0"})
except httpx.HTTPError as exc:
logger.warning("Document proxy fetch failed for {}: {}", url, exc)
raise HTTPException(status_code=502, detail="Failed to fetch document") from exc
if resp.status_code != 200:
span.set_attribute("upstream.status", resp.status_code)
raise HTTPException(status_code=502, detail=f"Upstream returned {resp.status_code}")
content = resp.content
if len(content) > _MAX_BYTES:
raise HTTPException(status_code=413, detail="Document too large to display")
# Trust the upstream content-type when present; default to PDF (the common case).
content_type = (resp.headers.get("content-type") or "application/pdf").split(";")[0].strip()
if not content_type:
content_type = "application/pdf"
span.set_attribute("document.content_type", content_type)
return Response(
content=content,
media_type=content_type,
headers={
"Cache-Control": "public, max-age=3600",
"Content-Disposition": "inline",
},
)