Coverage for src/lexigram/web/errors/problem_detail.py: 59%
37 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""RFC 7807 Problem Details for HTTP APIs.
3Provides standardized error response format.
4"""
6from __future__ import annotations
8from dataclasses import dataclass, field
9from typing import Any
12@dataclass(frozen=True)
13class ProblemDetail:
14 """RFC 7807 Problem Details for HTTP APIs.
16 Attributes:
17 type: URI identifying the problem type
18 title: Short summary of the problem
19 status: HTTP status code
20 detail: Human-readable explanation
21 instance: URI for this specific occurrence
22 errors: Additional validation errors (extension)
23 """
25 type: str = "about:blank"
26 title: str = "An error occurred"
27 status: int = 500
28 detail: str = "An unexpected error occurred"
29 instance: str | None = None
30 errors: list[dict[str, Any]] = field(default_factory=list)
32 def to_dict(self) -> dict[str, Any]:
33 """Convert to dictionary for JSON serialization."""
34 result = {
35 "type": self.type,
36 "title": self.title,
37 "status": self.status,
38 "detail": self.detail,
39 }
40 if self.instance:
41 result["instance"] = self.instance
42 if self.errors:
43 result["errors"] = self.errors
44 return result
46 @classmethod
47 def from_exception(
48 cls,
49 exc: Exception,
50 status: int = 500,
51 debug: bool = False,
52 **kwargs: Any,
53 ) -> ProblemDetail:
54 """Create a ProblemDetail from an exception.
56 When debug is False (default), server-error responses (5xx) use a
57 generic detail string to prevent internal information from leaking
58 to API consumers. Set debug=True only in development environments.
59 """
60 detail = str(exc) if debug or status < 500 else "An unexpected error occurred"
61 return cls(
62 status=status,
63 detail=detail,
64 **kwargs,
65 )
67 @classmethod
68 def validation_error(
69 cls,
70 errors: list[dict[str, Any]],
71 detail: str = "Validation failed",
72 ) -> ProblemDetail:
73 """Create a validation error ProblemDetail."""
74 return cls(
75 type="urn:lexigram:validation-error",
76 title="Validation Error",
77 status=400,
78 detail=detail,
79 errors=errors,
80 )
82 @classmethod
83 def not_found(
84 cls,
85 resource: str,
86 identifier: str | None = None,
87 ) -> ProblemDetail:
88 """Create a not found ProblemDetail."""
89 detail = f"Resource '{resource}' not found"
90 if identifier:
91 detail += f": {identifier}"
92 return cls(
93 type="urn:lexigram:not-found",
94 title="Not Found",
95 status=404,
96 detail=detail,
97 )
99 @classmethod
100 def bad_request(
101 cls,
102 detail: str,
103 errors: list[dict[str, Any]] | None = None,
104 ) -> ProblemDetail:
105 """Create a bad request ProblemDetail."""
106 return cls(
107 type="urn:lexigram:bad-request",
108 title="Bad Request",
109 status=400,
110 detail=detail,
111 errors=errors or [],
112 )
114 @classmethod
115 def internal_error(
116 cls,
117 detail: str = "An internal error occurred",
118 ) -> ProblemDetail:
119 """Create an internal error ProblemDetail."""
120 return cls(
121 type="urn:lexigram:internal-error",
122 title="Internal Server Error",
123 status=500,
124 detail=detail,
125 )