first
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
||||
"""Typované výjimky + centrální exception handlery.
|
||||
|
||||
Platí pravidlo: žádná tichá selhání — každá chyba se loguje (bez secrets).
|
||||
Chyby upstreamu (PPL CPL API) se mapují na stejné/odpovídající HTTP statusy,
|
||||
tělo problem+json z PPL se předává v poli `detail`, ať klient vidí přesnou příčinu.
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from .logging_config import get_logger
|
||||
|
||||
log = get_logger("pplcpl.errors")
|
||||
|
||||
|
||||
class CplServiceError(Exception):
|
||||
"""Základní chyba služby s HTTP status kódem."""
|
||||
|
||||
status_code = 500
|
||||
|
||||
def __init__(self, message: str, status_code: int | None = None, detail: Any = None):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
if status_code is not None:
|
||||
self.status_code = status_code
|
||||
self.detail = detail
|
||||
|
||||
|
||||
class CredentialsError(CplServiceError):
|
||||
"""Chybějící / PPL odmítnuté přihlašovací údaje (X- hlavičky)."""
|
||||
|
||||
status_code = 401
|
||||
|
||||
|
||||
class ForbiddenError(CplServiceError):
|
||||
"""PPL odmítlo přístup (chybí oprávnění / role k dané metodě)."""
|
||||
|
||||
status_code = 403
|
||||
|
||||
|
||||
class BadRequestError(CplServiceError):
|
||||
"""Neplatný vstup — validační chyba na straně PPL nebo této služby."""
|
||||
|
||||
status_code = 400
|
||||
|
||||
|
||||
class NotFoundError(CplServiceError):
|
||||
"""Záznam (batch, zásilka, objednávka) v PPL neexistuje."""
|
||||
|
||||
status_code = 404
|
||||
|
||||
|
||||
class RateLimitError(CplServiceError):
|
||||
"""Upstream rate limit (HTTP 429)."""
|
||||
|
||||
status_code = 429
|
||||
|
||||
|
||||
class UpstreamError(CplServiceError):
|
||||
"""Výpadek / neočekávaná chyba PPL CPL API."""
|
||||
|
||||
status_code = 502
|
||||
|
||||
|
||||
def _parse_detail(body: str, content_type: str) -> Any:
|
||||
"""Problem+json z PPL vracíme jako objekt, jiná těla jako ořezaný text."""
|
||||
if "json" in (content_type or ""):
|
||||
import json
|
||||
|
||||
try:
|
||||
return json.loads(body)
|
||||
except ValueError:
|
||||
log.warning("PPL vrátilo nevalidní JSON v chybové odpovědi.")
|
||||
return (body or "")[:1000]
|
||||
|
||||
|
||||
def raise_for_upstream(status: int, body: str, content_type: str = "") -> None:
|
||||
"""Zmapuje chybový HTTP status z PPL CPL API na správnou chybu služby.
|
||||
|
||||
- 400/422 → 400 (validační chyba — detail obsahuje problem+json z PPL)
|
||||
- 401 → 401 (PPL odmítlo token / přihlašovací údaje)
|
||||
- 403 → 403 (chybějící oprávnění k metodě)
|
||||
- 404 → 404 (batch / zásilka / objednávka neexistuje)
|
||||
- 429 → 429 (rate limit)
|
||||
- jinak → 502 (výpadek / neočekávaná chyba PPL)
|
||||
"""
|
||||
detail = _parse_detail(body, content_type)
|
||||
if status in (400, 422):
|
||||
raise BadRequestError(
|
||||
f"PPL CPL API odmítlo požadavek (HTTP {status}) — validační chyba.",
|
||||
detail=detail,
|
||||
)
|
||||
if status == 401:
|
||||
raise CredentialsError(
|
||||
"PPL CPL API odmítlo přihlašovací údaje / token (HTTP 401).",
|
||||
detail=detail,
|
||||
)
|
||||
if status == 403:
|
||||
raise ForbiddenError(
|
||||
"PPL CPL API odmítlo přístup (HTTP 403) — chybí oprávnění k metodě.",
|
||||
detail=detail,
|
||||
)
|
||||
if status == 404:
|
||||
raise NotFoundError(
|
||||
"Záznam v PPL CPL API neexistuje (HTTP 404).",
|
||||
detail=detail,
|
||||
)
|
||||
if status == 429:
|
||||
raise RateLimitError("PPL CPL API rate limit (HTTP 429).", detail=detail)
|
||||
raise UpstreamError(f"PPL CPL API vrátilo chybu {status}.", detail=detail)
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI) -> None:
|
||||
@app.exception_handler(CplServiceError)
|
||||
async def _handle_service_error(request: Request, exc: CplServiceError):
|
||||
log.warning(
|
||||
"%s on %s: %s",
|
||||
exc.__class__.__name__,
|
||||
request.url.path,
|
||||
exc.message,
|
||||
)
|
||||
body = {"error": exc.__class__.__name__, "message": exc.message}
|
||||
if exc.detail is not None:
|
||||
body["detail"] = exc.detail
|
||||
return JSONResponse(status_code=exc.status_code, content=body)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def _handle_unexpected(request: Request, exc: Exception):
|
||||
# Nelogujeme celý stack s možnými secrets ve vstupu; logujeme typ + zprávu.
|
||||
log.error(
|
||||
"Unhandled %s on %s: %s",
|
||||
exc.__class__.__name__,
|
||||
request.url.path,
|
||||
exc,
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "InternalError", "message": "Neočekávaná chyba serveru."},
|
||||
)
|
||||
Reference in New Issue
Block a user