Coverage for src/lexigram/graphql/core/error_formatter.py: 96%
45 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"""Error formatting for GraphQL responses.
3This module provides error masking and formatting based on configuration.
4"""
6from __future__ import annotations
8import traceback
9from typing import Any
11from lexigram.graphql.config import ErrorConfig
12from lexigram.graphql.exceptions import GraphQLError
15def _is_safe_error(error: Exception) -> bool:
16 """Check if an error is safe to show to users.
18 Args:
19 error: The error to check.
21 Returns:
22 True if the error message can be safely shown to users.
23 """
24 # Check for safe attribute on GraphQLError subclasses
25 if isinstance(error, GraphQLError):
26 return getattr(error, "safe", False)
27 return False
30class ErrorFormatter:
31 """Format GraphQL errors for client consumption.
33 Applies masking rules based on ErrorConfig to prevent
34 internal errors from leaking to clients in production.
35 """
37 def __init__(self, config: ErrorConfig):
38 """Initialize the error formatter.
40 Args:
41 config: Error configuration.
42 """
43 self._config = config
45 def format_error(
46 self,
47 error: Exception,
48 ) -> dict[str, Any]:
49 """Format an error for the response.
51 Args:
52 error: The exception to format (GraphQLError or Strawberry ErrorExtension).
54 Returns:
55 Formatted error dictionary.
56 """
57 # Handle Strawberry's ErrorExtension type
58 if hasattr(error, "message"):
59 message = str(error.message)
60 elif isinstance(error, GraphQLError):
61 message = error.message
62 else:
63 message = str(error)
65 result: dict[str, Any] = {"message": message}
67 # Add extensions for error code if available
68 if isinstance(error, GraphQLError):
69 extensions: dict[str, Any] = {}
71 if hasattr(error, "code") and error.code:
72 extensions["code"] = error.code
74 # Add details if available
75 if hasattr(error, "details") and error.details:
76 extensions["details"] = error.details
78 # Add hint if available
79 if hasattr(error, "hint") and error.hint:
80 extensions["hint"] = error.hint
82 if extensions:
83 result["extensions"] = extensions
84 elif hasattr(error, "extensions") and error.extensions:
85 result["extensions"] = error.extensions
87 # Mask internal errors in production (unless error is marked as safe)
88 if self._config.mask_errors and not _is_safe_error(error):
89 result["message"] = "Internal server error"
90 # Clear extensions that might leak info
91 if "extensions" in result:
92 result["extensions"] = {"code": "INTERNAL_ERROR"}
94 # Include stacktrace only in debug mode
95 if self._config.include_stacktrace and self._config.debug_mode:
96 if "extensions" not in result:
97 result["extensions"] = {}
98 result["extensions"]["stacktrace"] = traceback.format_exception(
99 type(error),
100 error,
101 error.__traceback__,
102 )
104 return result
106 def format_errors(
107 self,
108 errors: list[Exception],
109 ) -> list[dict[str, Any]]:
110 """Format multiple errors for the response.
112 Args:
113 errors: List of exceptions to format.
115 Returns:
116 List of formatted error dictionaries.
117 """
118 return [self.format_error(error) for error in errors]
121# Default error formatter factory
122def create_error_formatter(config: ErrorConfig | None = None) -> ErrorFormatter:
123 """Create an ErrorFormatter with the given config.
125 Args:
126 config: Error configuration. Uses defaults if None.
128 Returns:
129 Configured ErrorFormatter.
130 """
131 return ErrorFormatter(config or ErrorConfig())
134__all__ = [
135 "ErrorFormatter",
136 "create_error_formatter",
137]