Praneshrajan15's picture
Deploy DataForge playground API
13fe504 verified
Raw
History Blame Contribute Delete
3.06 kB
"""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