|
2 | 2 | from fastapi import Request, status |
3 | 3 | from fastapi.responses import JSONResponse |
4 | 4 |
|
| 5 | +from backend.services.exceptions import ( |
| 6 | + DraftNotFoundError, |
| 7 | + PublishingFailedError, |
| 8 | + UnsupportedPlatformError, |
| 9 | +) |
| 10 | + |
5 | 11 | logger = structlog.get_logger() |
6 | 12 |
|
| 13 | +_PROBLEM_TYPE_BASE = "https://serotonin-script.internal/errors" |
| 14 | + |
| 15 | + |
| 16 | +def _problem( |
| 17 | + request: Request, |
| 18 | + status_code: int, |
| 19 | + title: str, |
| 20 | + detail: str, |
| 21 | + extra: dict[str, object] | None = None, |
| 22 | +) -> JSONResponse: |
| 23 | + """Return an RFC 7807 Problem Details response.""" |
| 24 | + body: dict[str, object] = { |
| 25 | + "type": f"{_PROBLEM_TYPE_BASE}/{title.lower().replace(' ', '-')}", |
| 26 | + "title": title, |
| 27 | + "status": status_code, |
| 28 | + "detail": detail, |
| 29 | + "instance": request.url.path, |
| 30 | + } |
| 31 | + if extra: |
| 32 | + body.update(extra) |
| 33 | + return JSONResponse(status_code=status_code, content=body) |
| 34 | + |
| 35 | + |
| 36 | +async def domain_exception_handler(request: Request, exc: Exception) -> JSONResponse: |
| 37 | + """Map domain errors to structured RFC 7807 HTTP responses.""" |
| 38 | + if isinstance(exc, DraftNotFoundError): |
| 39 | + return _problem( |
| 40 | + request, |
| 41 | + status.HTTP_404_NOT_FOUND, |
| 42 | + "Draft Not Found", |
| 43 | + str(exc), |
| 44 | + {"draft_id": str(exc.draft_id)}, |
| 45 | + ) |
| 46 | + if isinstance(exc, UnsupportedPlatformError): |
| 47 | + return _problem( |
| 48 | + request, |
| 49 | + status.HTTP_422_UNPROCESSABLE_ENTITY, |
| 50 | + "Unsupported Platform", |
| 51 | + str(exc), |
| 52 | + {"platform": exc.platform}, |
| 53 | + ) |
| 54 | + if isinstance(exc, PublishingFailedError): |
| 55 | + logger.error( |
| 56 | + "publishing_failed", |
| 57 | + platform=str(exc.platform), |
| 58 | + reason=exc.reason, |
| 59 | + path=request.url.path, |
| 60 | + ) |
| 61 | + return _problem( |
| 62 | + request, |
| 63 | + status.HTTP_502_BAD_GATEWAY, |
| 64 | + "Publishing Failed", |
| 65 | + str(exc), |
| 66 | + {"platform": str(exc.platform)}, |
| 67 | + ) |
| 68 | + # Catch-all for any future DomainError subclasses |
| 69 | + logger.error("unhandled_domain_error", error=str(exc), path=request.url.path) |
| 70 | + return _problem( |
| 71 | + request, |
| 72 | + status.HTTP_400_BAD_REQUEST, |
| 73 | + "Domain Error", |
| 74 | + str(exc), |
| 75 | + ) |
| 76 | + |
7 | 77 |
|
8 | 78 | async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse: |
9 | | - """Handle all unhandled exceptions and return a 500 JSON response.""" |
| 79 | + """Handle all unhandled exceptions and return a 500 RFC 7807 response.""" |
10 | 80 | logger.error( |
11 | 81 | "unhandled_exception", |
12 | 82 | error=str(exc), |
13 | 83 | error_type=type(exc).__name__, |
14 | 84 | path=request.url.path, |
15 | 85 | method=request.method, |
16 | 86 | ) |
17 | | - return JSONResponse( |
18 | | - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
19 | | - content={"detail": "Internal server error occurred."}, |
| 87 | + return _problem( |
| 88 | + request, |
| 89 | + status.HTTP_500_INTERNAL_SERVER_ERROR, |
| 90 | + "Internal Server Error", |
| 91 | + "An unexpected error occurred.", |
20 | 92 | ) |
0 commit comments