1111from aiohttp .web_exceptions import HTTPError
1212from aiohttp .web_request import Request
1313from aiohttp .web_response import StreamResponse
14- from common_library .error_codes import create_error_code
14+ from common_library .error_codes import ErrorCodeStr , create_error_code
1515from common_library .json_serialization import json_dumps , json_loads
1616from common_library .user_messages import user_message
17+ from models_library .basic_types import IDStr
1718from models_library .rest_error import ErrorGet , ErrorItemType , LogMessageType
19+ from servicelib .rest_constants import RESPONSE_MODEL_POLICY
20+ from servicelib .status_codes_utils import is_5xx_server_error
1821
1922from ..logging_errors import create_troubleshotting_log_kwargs
2023from ..mimetype_constants import MIMETYPE_APPLICATION_JSON
2124from ..rest_responses import is_enveloped_from_map , is_enveloped_from_text
22- from ..utils import is_production_environ
25+ from ..status_codes_utils import get_code_description
2326from . import status
2427from .rest_responses import (
2528 create_data_response ,
2629 create_http_error ,
2730 safe_status_message ,
2831 wrap_as_envelope ,
2932)
30- from .rest_utils import EnvelopeFactory
3133from .typing_extension import Handler , Middleware
3234from .web_exceptions_extension import get_http_error_class_or_none
3335
@@ -46,34 +48,28 @@ def is_api_request(request: web.Request, api_version: str) -> bool:
4648 return bool (request .path .startswith (base_path ))
4749
4850
49- def _handle_unexpected_exception_as_500 (
50- request : web .BaseRequest ,
51- exception : Exception ,
52- * ,
53- skip_internal_error_details : bool ,
54- ) -> web .HTTPInternalServerError :
55- """Process unexpected exceptions and return them as HTTP errors with proper formatting.
51+ def _create_error_context (
52+ request : web .BaseRequest , exception : Exception
53+ ) -> tuple [ErrorCodeStr , dict [str , Any ]]:
54+ """Create error code and context for logging purposes.
5655
57- IMPORTANT: this function cannot throw exceptions, as it is called
56+ Returns:
57+ Tuple of (error_code, error_context)
5858 """
5959 error_code = create_error_code (exception )
6060 error_context : dict [str , Any ] = {
6161 "request.remote" : f"{ request .remote } " ,
6262 "request.method" : f"{ request .method } " ,
6363 "request.path" : f"{ request .path } " ,
6464 }
65+ return error_code , error_context
6566
66- user_error_msg = _FMSG_INTERNAL_ERROR_USER_FRIENDLY
67-
68- http_error = create_http_error (
69- exception ,
70- user_error_msg ,
71- web .HTTPInternalServerError ,
72- skip_internal_error_details = skip_internal_error_details ,
73- error_code = error_code ,
74- )
7567
76- error_context ["http_error" ] = http_error
68+ def _log_5xx_server_error (
69+ request : web .BaseRequest , exception : Exception , user_error_msg : str
70+ ) -> ErrorCodeStr :
71+ """Log 5XX server errors with error code and context."""
72+ error_code , error_context = _create_error_context (request , exception )
7773
7874 _logger .exception (
7975 ** create_troubleshotting_log_kwargs (
@@ -83,33 +79,78 @@ def _handle_unexpected_exception_as_500(
8379 error_code = error_code ,
8480 )
8581 )
82+ return error_code
83+
84+
85+ def _handle_unexpected_exception_as_500 (
86+ request : web .BaseRequest , exception : Exception
87+ ) -> web .HTTPInternalServerError :
88+ """Process unexpected exceptions and return them as HTTP errors with proper formatting.
89+
90+ IMPORTANT: this function cannot throw exceptions, as it is called
91+ """
92+ error_code , error_context = _create_error_context (request , exception )
93+ user_error_msg = _FMSG_INTERNAL_ERROR_USER_FRIENDLY
94+
95+ error_context ["http_error" ] = http_error = create_http_error (
96+ exception ,
97+ user_error_msg ,
98+ web .HTTPInternalServerError ,
99+ error_code = error_code ,
100+ )
101+
102+ _log_5xx_server_error (request , exception , user_error_msg )
103+
86104 return http_error
87105
88106
89107def _handle_http_error (
90108 request : web .BaseRequest , exception : web .HTTPError
91109) -> web .HTTPError :
92- """Handle standard HTTP errors by ensuring they're properly formatted."""
110+ """Handle standard HTTP errors by ensuring they're properly formatted.
111+
112+ NOTE: this needs further refactoring to avoid code duplication
113+ """
93114 assert request # nosec
115+ assert not exception .empty_body , "HTTPError should not have an empty body" # nosec
116+
94117 exception .content_type = MIMETYPE_APPLICATION_JSON
95118 if exception .reason :
96119 exception .set_status (
97120 exception .status , safe_status_message (message = exception .reason )
98121 )
99122
100123 if not exception .text or not is_enveloped_from_text (exception .text ):
101- error_message = exception .text or exception .reason or "Unexpected error"
124+ # NOTE: aiohttp.HTTPException creates `text = f"{self.status}: {self.reason}"`
125+ user_error_msg = exception .text or "Unexpected error"
126+
127+ error_code : IDStr | None = None
128+ if is_5xx_server_error (exception .status ):
129+ error_code = IDStr (
130+ _log_5xx_server_error (request , exception , user_error_msg )
131+ )
132+
102133 error_model = ErrorGet (
103134 errors = [
104- ErrorItemType .from_error (exception ),
135+ ErrorItemType (
136+ code = exception .__class__ .__name__ ,
137+ message = user_error_msg ,
138+ resource = None ,
139+ field = None ,
140+ ),
105141 ],
106142 status = exception .status ,
107143 logs = [
108- LogMessageType (message = error_message , level = "ERROR" ),
144+ LogMessageType (message = user_error_msg , level = "ERROR" ),
109145 ],
110- message = error_message ,
146+ message = user_error_msg ,
147+ support_id = error_code ,
148+ )
149+ exception .text = json_dumps (
150+ wrap_as_envelope (
151+ error = error_model .model_dump (mode = "json" , ** RESPONSE_MODEL_POLICY )
152+ )
111153 )
112- exception .text = EnvelopeFactory (error = error_model ).as_text ()
113154
114155 return exception
115156
@@ -137,10 +178,8 @@ def _handle_http_successful(
137178
138179def _handle_exception_as_http_error (
139180 request : web .Request ,
140- exception : Exception ,
181+ exception : NotImplementedError | TimeoutError ,
141182 status_code : int ,
142- * ,
143- skip_internal_error_details : bool ,
144183) -> HTTPError :
145184 """
146185 Generic handler for exceptions that map to specific HTTP status codes.
@@ -155,16 +194,15 @@ def _handle_exception_as_http_error(
155194 )
156195 raise ValueError (msg )
157196
158- return create_http_error (
159- exception ,
160- f" { exception } " ,
161- http_error_cls ,
162- skip_internal_error_details = skip_internal_error_details ,
163- )
197+ user_error_msg = get_code_description ( status_code )
198+
199+ if is_5xx_server_error ( status_code ):
200+ _log_5xx_server_error ( request , exception , user_error_msg )
201+
202+ return create_http_error ( exception , user_error_msg , http_error_cls )
164203
165204
166205def error_middleware_factory (api_version : str ) -> Middleware :
167- _is_prod : bool = is_production_environ ()
168206
169207 @web .middleware
170208 async def _middleware_handler (request : web .Request , handler : Handler ):
@@ -189,27 +227,19 @@ async def _middleware_handler(request: web.Request, handler: Handler):
189227
190228 except NotImplementedError as exc :
191229 result = _handle_exception_as_http_error (
192- request ,
193- exc ,
194- status .HTTP_501_NOT_IMPLEMENTED ,
195- skip_internal_error_details = _is_prod ,
230+ request , exc , status .HTTP_501_NOT_IMPLEMENTED
196231 )
197232
198233 except TimeoutError as exc :
199234 result = _handle_exception_as_http_error (
200- request ,
201- exc ,
202- status .HTTP_504_GATEWAY_TIMEOUT ,
203- skip_internal_error_details = _is_prod ,
235+ request , exc , status .HTTP_504_GATEWAY_TIMEOUT
204236 )
205237
206238 except Exception as exc : # pylint: disable=broad-except
207239 #
208240 # Last resort for unexpected exceptions (including those raise by the exception handlers!)
209241 #
210- result = _handle_unexpected_exception_as_500 (
211- request , exc , skip_internal_error_details = _is_prod
212- )
242+ result = _handle_unexpected_exception_as_500 (request , exc )
213243
214244 return result
215245
@@ -230,7 +260,6 @@ def envelope_middleware_factory(
230260 api_version : str ,
231261) -> Callable [..., Awaitable [StreamResponse ]]:
232262 # FIXME: This data conversion is very error-prone. Use decorators instead!
233- _is_prod : bool = is_production_environ ()
234263
235264 @web .middleware
236265 async def _middleware_handler (
0 commit comments