"""RFC 9457 problem details helpers for FastAPI surfaces.""" from __future__ import annotations from collections.abc import Mapping from typing import Any from fastapi import HTTPException, Request from fastapi.responses import JSONResponse from pydantic import BaseModel, ConfigDict, Field class ProblemDetail(BaseModel): """RFC 9457 problem detail response with extension members.""" type: str = Field(default="about:blank") title: str status: int detail: str instance: str | None = None model_config = ConfigDict(strict=True, extra="allow") def problem_body( *, status: int, title: str, detail: str, type_: str = "about:blank", instance: str | None = None, **extensions: Any, ) -> dict[str, Any]: """Build a problem details JSON object.""" body = ProblemDetail( type=type_, title=title, status=status, detail=detail, instance=instance, **extensions, ) return body.model_dump(mode="json", exclude_none=True) def problem_response( *, status: int, title: str, detail: str, type_: str = "about:blank", instance: str | None = None, headers: Mapping[str, str] | None = None, **extensions: Any, ) -> JSONResponse: """Return an RFC 9457 JSON response.""" return JSONResponse( status_code=status, content=problem_body( status=status, title=title, detail=detail, type_=type_, instance=instance, **extensions, ), headers=headers, media_type="application/problem+json", ) async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: """Normalize FastAPI HTTPException values into problem details.""" raw_detail = exc.detail extensions: dict[str, Any] = {} if isinstance(raw_detail, dict): error_code = str(raw_detail.get("error", "http_error")) message = str(raw_detail.get("message") or raw_detail.get("detail") or error_code) extensions.update(raw_detail) else: error_code = "http_error" message = str(raw_detail) request_id = getattr(request.state, "dataforge_request_id", None) if isinstance(request_id, str) and request_id and "request_id" not in extensions: extensions["request_id"] = request_id return problem_response( status=exc.status_code, type_=f"https://dataforge.local/problems/{error_code}", title=error_code.replace("_", " ").title(), detail=message, instance=str(request.url.path), headers=exc.headers, **extensions, ) async def problem_exception_handler(request: Request, exc: Exception) -> JSONResponse: """Adapter with the broad exception signature Starlette expects.""" if isinstance(exc, HTTPException): return await http_exception_handler(request, exc) raise exc