"""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."}, )