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

1"""RFC 7807 Problem Details for HTTP APIs. 

2 

3Provides standardized error response format. 

4""" 

5 

6from __future__ import annotations 

7 

8from dataclasses import dataclass, field 

9from typing import Any 

10 

11 

12@dataclass(frozen=True) 

13class ProblemDetail: 

14 """RFC 7807 Problem Details for HTTP APIs. 

15 

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 """ 

24 

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) 

31 

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 

45 

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. 

55 

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 ) 

66 

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 ) 

81 

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 ) 

98 

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 ) 

113 

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 )