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
16+ from models_library .basic_types import IDStr
1617from models_library .rest_error import ErrorGet , ErrorItemType , LogMessageType
18+ from servicelib .rest_constants import RESPONSE_MODEL_POLICY
19+ from servicelib .status_codes_utils import is_5xx_server_error
1720
1821from ..logging_errors import create_troubleshotting_log_kwargs
1922from ..mimetype_constants import MIMETYPE_APPLICATION_JSON
2023from ..rest_responses import is_enveloped_from_map , is_enveloped_from_text
21- from ..utils import is_production_environ
24+ from ..status_codes_utils import get_code_description
2225from . import status
2326from .rest_responses import (
2427 create_data_response ,
2528 create_http_error ,
2629 safe_status_message ,
2730 wrap_as_envelope ,
2831)
29- from .rest_utils import EnvelopeFactory
3032from .typing_extension import Handler , Middleware
3133from .web_exceptions_extension import get_http_error_class_or_none
3234
@@ -45,34 +47,28 @@ def is_api_request(request: web.Request, api_version: str) -> bool:
4547 return bool (request .path .startswith (base_path ))
4648
4749
48- def _handle_unexpected_exception_as_500 (
49- request : web .BaseRequest ,
50- exception : Exception ,
51- * ,
52- skip_internal_error_details : bool ,
53- ) -> web .HTTPInternalServerError :
54- """Process unexpected exceptions and return them as HTTP errors with proper formatting.
50+ def _create_error_context (
51+ request : web .BaseRequest , exception : Exception
52+ ) -> tuple [ErrorCodeStr , dict [str , Any ]]:
53+ """Create error code and context for logging purposes.
5554
56- IMPORTANT: this function cannot throw exceptions, as it is called
55+ Returns:
56+ Tuple of (error_code, error_context)
5757 """
5858 error_code = create_error_code (exception )
5959 error_context : dict [str , Any ] = {
6060 "request.remote" : f"{ request .remote } " ,
6161 "request.method" : f"{ request .method } " ,
6262 "request.path" : f"{ request .path } " ,
6363 }
64+ return error_code , error_context
6465
65- user_error_msg = _FMSG_INTERNAL_ERROR_USER_FRIENDLY
66-
67- http_error = create_http_error (
68- exception ,
69- user_error_msg ,
70- web .HTTPInternalServerError ,
71- skip_internal_error_details = skip_internal_error_details ,
72- error_code = error_code ,
73- )
7466
75- error_context ["http_error" ] = http_error
67+ def _log_5xx_server_error (
68+ request : web .BaseRequest , exception : Exception , user_error_msg : str
69+ ) -> ErrorCodeStr :
70+ """Log 5XX server errors with error code and context."""
71+ error_code , error_context = _create_error_context (request , exception )
7672
7773 _logger .exception (
7874 ** create_troubleshotting_log_kwargs (
@@ -82,33 +78,78 @@ def _handle_unexpected_exception_as_500(
8278 error_code = error_code ,
8379 )
8480 )
81+ return error_code
82+
83+
84+ def _handle_unexpected_exception_as_500 (
85+ request : web .BaseRequest , exception : Exception
86+ ) -> web .HTTPInternalServerError :
87+ """Process unexpected exceptions and return them as HTTP errors with proper formatting.
88+
89+ IMPORTANT: this function cannot throw exceptions, as it is called
90+ """
91+ error_code , error_context = _create_error_context (request , exception )
92+ user_error_msg = _FMSG_INTERNAL_ERROR_USER_FRIENDLY
93+
94+ error_context ["http_error" ] = http_error = create_http_error (
95+ exception ,
96+ user_error_msg ,
97+ web .HTTPInternalServerError ,
98+ error_code = error_code ,
99+ )
100+
101+ _log_5xx_server_error (request , exception , user_error_msg )
102+
85103 return http_error
86104
87105
88106def _handle_http_error (
89107 request : web .BaseRequest , exception : web .HTTPError
90108) -> web .HTTPError :
91- """Handle standard HTTP errors by ensuring they're properly formatted."""
109+ """Handle standard HTTP errors by ensuring they're properly formatted.
110+
111+ NOTE: this needs further refactoring to avoid code duplication
112+ """
92113 assert request # nosec
114+ assert not exception .empty_body , "HTTPError should not have an empty body" # nosec
115+
93116 exception .content_type = MIMETYPE_APPLICATION_JSON
94117 if exception .reason :
95118 exception .set_status (
96119 exception .status , safe_status_message (message = exception .reason )
97120 )
98121
99122 if not exception .text or not is_enveloped_from_text (exception .text ):
100- error_message = exception .text or exception .reason or "Unexpected error"
123+ # NOTE: aiohttp.HTTPException creates `text = f"{self.status}: {self.reason}"`
124+ user_error_msg = exception .text or "Unexpected error"
125+
126+ error_code : IDStr | None = None
127+ if is_5xx_server_error (exception .status ):
128+ error_code = IDStr (
129+ _log_5xx_server_error (request , exception , user_error_msg )
130+ )
131+
101132 error_model = ErrorGet (
102133 errors = [
103- ErrorItemType .from_error (exception ),
134+ ErrorItemType (
135+ code = exception .__class__ .__name__ ,
136+ message = user_error_msg ,
137+ resource = None ,
138+ field = None ,
139+ ),
104140 ],
105141 status = exception .status ,
106142 logs = [
107- LogMessageType (message = error_message , level = "ERROR" ),
143+ LogMessageType (message = user_error_msg , level = "ERROR" ),
108144 ],
109- message = error_message ,
145+ message = user_error_msg ,
146+ support_id = error_code ,
147+ )
148+ exception .text = json_dumps (
149+ wrap_as_envelope (
150+ error = error_model .model_dump (mode = "json" , ** RESPONSE_MODEL_POLICY )
151+ )
110152 )
111- exception .text = EnvelopeFactory (error = error_model ).as_text ()
112153
113154 return exception
114155
@@ -136,10 +177,8 @@ def _handle_http_successful(
136177
137178def _handle_exception_as_http_error (
138179 request : web .Request ,
139- exception : Exception ,
180+ exception : NotImplementedError | TimeoutError ,
140181 status_code : int ,
141- * ,
142- skip_internal_error_details : bool ,
143182) -> HTTPError :
144183 """
145184 Generic handler for exceptions that map to specific HTTP status codes.
@@ -154,16 +193,15 @@ def _handle_exception_as_http_error(
154193 )
155194 raise ValueError (msg )
156195
157- return create_http_error (
158- exception ,
159- f" { exception } " ,
160- http_error_cls ,
161- skip_internal_error_details = skip_internal_error_details ,
162- )
196+ user_error_msg = get_code_description ( status_code )
197+
198+ if is_5xx_server_error ( status_code ):
199+ _log_5xx_server_error ( request , exception , user_error_msg )
200+
201+ return create_http_error ( exception , user_error_msg , http_error_cls )
163202
164203
165204def error_middleware_factory (api_version : str ) -> Middleware :
166- _is_prod : bool = is_production_environ ()
167205
168206 @web .middleware
169207 async def _middleware_handler (request : web .Request , handler : Handler ):
@@ -188,27 +226,19 @@ async def _middleware_handler(request: web.Request, handler: Handler):
188226
189227 except NotImplementedError as exc :
190228 result = _handle_exception_as_http_error (
191- request ,
192- exc ,
193- status .HTTP_501_NOT_IMPLEMENTED ,
194- skip_internal_error_details = _is_prod ,
229+ request , exc , status .HTTP_501_NOT_IMPLEMENTED
195230 )
196231
197232 except TimeoutError as exc :
198233 result = _handle_exception_as_http_error (
199- request ,
200- exc ,
201- status .HTTP_504_GATEWAY_TIMEOUT ,
202- skip_internal_error_details = _is_prod ,
234+ request , exc , status .HTTP_504_GATEWAY_TIMEOUT
203235 )
204236
205237 except Exception as exc : # pylint: disable=broad-except
206238 #
207239 # Last resort for unexpected exceptions (including those raise by the exception handlers!)
208240 #
209- result = _handle_unexpected_exception_as_500 (
210- request , exc , skip_internal_error_details = _is_prod
211- )
241+ result = _handle_unexpected_exception_as_500 (request , exc )
212242
213243 return result
214244
@@ -229,7 +259,6 @@ def envelope_middleware_factory(
229259 api_version : str ,
230260) -> Callable [..., Awaitable [StreamResponse ]]:
231261 # FIXME: This data conversion is very error-prone. Use decorators instead!
232- _is_prod : bool = is_production_environ ()
233262
234263 @web .middleware
235264 async def _middleware_handler (
0 commit comments