"""Stable RFC 9457-style problem responses for public API failures.""" from __future__ import annotations from dataclasses import dataclass from typing import Any from fastapi import Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse PROBLEM_MEDIA_TYPE = "application/problem+json" PROBLEM_BASE = "https://geological-rag.local/problems" @dataclass(frozen=True, slots=True) class ApiProblem(Exception): """An intentionally public and sanitized application failure.""" status: int code: str title: str detail: str def __post_init__(self) -> None: Exception.__init__(self, self.code) def problem_payload( *, status: int, code: str, title: str, detail: str, trace_id: str, field_errors: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: """Build the only public error envelope used by formal product routes.""" return { "type": f"{PROBLEM_BASE}/{code.lower().replace('_', '-')}", "title": title, "status": status, "code": code, "detail": detail, "trace_id": trace_id, "field_errors": field_errors or [], } def api_problem_handler(request: Request, exc: ApiProblem) -> JSONResponse: trace_id = str(getattr(request.state, "trace_id", "unavailable")) return JSONResponse( status_code=exc.status, media_type=PROBLEM_MEDIA_TYPE, content=problem_payload( status=exc.status, code=exc.code, title=exc.title, detail=exc.detail, trace_id=trace_id, ), ) def request_validation_problem_handler( request: Request, exc: RequestValidationError, ) -> JSONResponse: """Return bounded validation metadata without echoing rejected input values.""" trace_id = str(getattr(request.state, "trace_id", "unavailable")) field_errors: list[dict[str, str]] = [] for error in exc.errors(): location = error.get("loc", ()) field = ".".join(str(part) for part in location if str(part) not in {"body", "query"}) error_type = error.get("type") field_errors.append( { "field": field[:240] or "request", "code": str(error_type)[:120] if error_type else "invalid_value", } ) return JSONResponse( status_code=422, media_type=PROBLEM_MEDIA_TYPE, content=problem_payload( status=422, code="REQUEST_VALIDATION_FAILED", title="Request validation failed", detail="One or more request fields did not satisfy the public API contract.", trace_id=trace_id, field_errors=field_errors[:50], ), )