124 lines
4.1 KiB
Python
124 lines
4.1 KiB
Python
from datetime import datetime, timezone
|
|
from typing import Any
|
|
import uuid
|
|
import logging
|
|
|
|
from fastapi import Request, status
|
|
from fastapi.exceptions import RequestValidationError
|
|
from fastapi.responses import JSONResponse
|
|
from starlette.exceptions import HTTPException
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def now_iso() -> str:
|
|
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
trace_id = getattr(request.state, "traceId", str(uuid.uuid4()))
|
|
is_json_error = any(e.get("type") in ("json_invalid", "json_decode", "value_error.jsondecode") for e in exc.errors())
|
|
|
|
if is_json_error:
|
|
return JSONResponse(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
content={
|
|
"code": "BAD_REQUEST",
|
|
"message": "Невалидный JSON",
|
|
"traceId": trace_id,
|
|
"timestamp": now_iso(),
|
|
"path": request.url.path,
|
|
"details": {"hint": "Проверьте запятые/кавычки"},
|
|
},
|
|
)
|
|
|
|
field_errors: list[dict[str, Any]] = []
|
|
for err in exc.errors():
|
|
loc = [str(x) for x in err.get("loc", []) if x != "body"]
|
|
field_name = ".".join(loc) if loc else "unknown"
|
|
|
|
msg = err.get("msg", "invalid")
|
|
if msg.startswith("Value error, "):
|
|
msg = msg.replace("Value error, ", "")
|
|
|
|
field_errors.append({
|
|
"field": field_name,
|
|
"issue": msg,
|
|
"rejectedValue": err.get("input", None),
|
|
})
|
|
|
|
return JSONResponse(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
content={
|
|
"code": "VALIDATION_FAILED",
|
|
"message": "Некоторые поля не прошли валидацию",
|
|
"traceId": trace_id,
|
|
"timestamp": now_iso(),
|
|
"path": request.url.path,
|
|
"fieldErrors": field_errors,
|
|
},
|
|
)
|
|
|
|
|
|
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
|
trace_id = getattr(request.state, "traceId", str(uuid.uuid4()))
|
|
|
|
message = str(exc.detail)
|
|
details = None
|
|
|
|
if isinstance(exc.detail, dict):
|
|
message = exc.detail.get("message", str(exc.detail))
|
|
details_data = {k: v for k, v in exc.detail.items() if k != "message"}
|
|
if details_data:
|
|
details = details_data
|
|
|
|
code = "HTTP_ERROR"
|
|
if exc.status_code == status.HTTP_409_CONFLICT:
|
|
code = "EMAIL_ALREADY_EXISTS" if "email" in message.lower() else "CONFLICT"
|
|
elif exc.status_code == status.HTTP_400_BAD_REQUEST:
|
|
code = "BAD_REQUEST"
|
|
elif exc.status_code == status.HTTP_401_UNAUTHORIZED:
|
|
code = "UNAUTHORIZED"
|
|
elif exc.status_code == status.HTTP_423_LOCKED:
|
|
code = "USER_INACTIVE"
|
|
elif exc.status_code == status.HTTP_403_FORBIDDEN:
|
|
code = "FORBIDDEN"
|
|
elif exc.status_code == status.HTTP_404_NOT_FOUND:
|
|
code = "NOT_FOUND"
|
|
if message == "Not Found":
|
|
message = "Ресурс не найден"
|
|
elif exc.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY:
|
|
code = "VALIDATION_FAILED"
|
|
|
|
content = {
|
|
"code": code,
|
|
"message": message,
|
|
"traceId": trace_id,
|
|
"timestamp": now_iso(),
|
|
"path": request.url.path,
|
|
}
|
|
|
|
if details:
|
|
content["details"] = details
|
|
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content=content,
|
|
)
|
|
|
|
|
|
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
trace_id = getattr(request.state, "traceId", str(uuid.uuid4()))
|
|
logger.exception("Unhandled exception on %s", request.url.path, exc_info=exc)
|
|
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={
|
|
"code": "INTERNAL_ERROR",
|
|
"message": "Внутренняя ошибка сервера",
|
|
"traceId": trace_id,
|
|
"timestamp": now_iso(),
|
|
"path": request.url.path,
|
|
},
|
|
) |