feat(api): standardize REST contracts and auth

This commit is contained in:
2026-07-30 20:38:51 +08:00
parent ae1a657554
commit ba947b616b
86 changed files with 54193 additions and 990 deletions
+89
View File
@@ -0,0 +1,89 @@
from __future__ import annotations
from typing import Any
from uuid import uuid4
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
class ProblemDetails(BaseModel):
"""RFC 9457 compatible error response used by the REST contract."""
type: str
title: str
status: int
detail: str
instance: str
code: str
trace_id: str
errors: list[dict[str, Any]] = Field(default_factory=list)
def _trace_id(request: Request) -> str:
return request.headers.get("X-Request-Id") or str(uuid4())
def _problem_response(
request: Request,
*,
status_code: int,
title: str,
detail: str,
code: str,
errors: list[dict[str, Any]] | None = None,
) -> JSONResponse:
problem = ProblemDetails(
type=f"https://tjwater.example/problems/{code.replace('_', '-')}",
title=title,
status=status_code,
detail=detail,
instance=request.url.path,
code=code,
trace_id=_trace_id(request),
errors=errors or [],
)
return JSONResponse(
status_code=status_code,
content=problem.model_dump(mode="json"),
media_type="application/problem+json",
)
def install_problem_details_handlers(app: FastAPI) -> None:
@app.exception_handler(RequestValidationError)
async def validation_error_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
return _problem_response(
request,
status_code=422,
title="Validation error",
detail="Request validation failed",
code="validation_error",
errors=exc.errors(),
)
@app.exception_handler(HTTPException)
async def http_error_handler(request: Request, exc: HTTPException) -> JSONResponse:
detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
code_by_status = {
401: "unauthenticated",
403: "forbidden",
404: "not_found",
409: "conflict",
422: "validation_error",
503: "dependency_unavailable",
}
return _problem_response(
request,
status_code=exc.status_code,
title=code_by_status.get(exc.status_code, "request_error")
.replace("_", " ")
.title(),
detail=detail,
code=code_by_status.get(exc.status_code, "request_error"),
)