88from typing import Any
99
1010from aiohttp import web
11+ from aiohttp .web_exceptions import HTTPError
1112from aiohttp .web_request import Request
1213from aiohttp .web_response import StreamResponse
1314from common_library .error_codes import create_error_code
1819from ..mimetype_constants import MIMETYPE_APPLICATION_JSON
1920from ..rest_responses import is_enveloped_from_map , is_enveloped_from_text
2021from ..utils import is_production_environ
22+ from . import status
2123from .rest_responses import (
2224 create_data_response ,
2325 create_http_error ,
2628)
2729from .rest_utils import EnvelopeFactory
2830from .typing_extension import Handler , Middleware
31+ from .web_exceptions_extension import get_http_error_class_or_none
2932
3033DEFAULT_API_VERSION = "v0"
3134_FMSG_INTERNAL_ERROR_USER_FRIENDLY = (
@@ -42,110 +45,172 @@ def is_api_request(request: web.Request, api_version: str) -> bool:
4245 return bool (request .path .startswith (base_path ))
4346
4447
45- def error_middleware_factory ( # noqa: C901
46- api_version : str ,
47- ) -> Middleware :
48- _is_prod : bool = is_production_environ ()
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.
55+
56+ IMPORTANT: this function cannot throw exceptions, as it is called
57+ """
58+ error_code = create_error_code (exception )
59+ error_context : dict [str , Any ] = {
60+ "request.remote" : f"{ request .remote } " ,
61+ "request.method" : f"{ request .method } " ,
62+ "request.path" : f"{ request .path } " ,
63+ }
64+
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+ )
74+
75+ error_context ["http_error" ] = http_error
4976
50- def _process_and_raise_unexpected_error (request : web .BaseRequest , err : Exception ):
51- error_code = create_error_code (err )
52- error_context : dict [str , Any ] = {
53- "request.remote" : f"{ request .remote } " ,
54- "request.method" : f"{ request .method } " ,
55- "request.path" : f"{ request .path } " ,
56- }
57-
58- user_error_msg = _FMSG_INTERNAL_ERROR_USER_FRIENDLY
59- http_error = create_http_error (
60- err ,
77+ _logger .exception (
78+ ** create_troubleshotting_log_kwargs (
6179 user_error_msg ,
62- web . HTTPInternalServerError ,
63- skip_internal_error_details = _is_prod ,
80+ error = exception ,
81+ error_context = error_context ,
6482 error_code = error_code ,
6583 )
66- _logger .exception (
67- ** create_troubleshotting_log_kwargs (
68- user_error_msg ,
69- error = err ,
70- error_context = error_context ,
71- error_code = error_code ,
72- )
84+ )
85+ return http_error
86+
87+
88+ def _handle_http_error (
89+ request : web .BaseRequest , exception : web .HTTPError
90+ ) -> web .HTTPError :
91+ """Handle standard HTTP errors by ensuring they're properly formatted."""
92+ assert request # nosec
93+ exception .content_type = MIMETYPE_APPLICATION_JSON
94+ if exception .reason :
95+ exception .set_status (
96+ exception .status , safe_status_message (message = exception .reason )
97+ )
98+
99+ if not exception .text or not is_enveloped_from_text (exception .text ):
100+ error_message = exception .text or exception .reason or "Unexpected error"
101+ error_model = ErrorGet (
102+ errors = [
103+ ErrorItemType .from_error (exception ),
104+ ],
105+ status = exception .status ,
106+ logs = [
107+ LogMessageType (message = error_message , level = "ERROR" ),
108+ ],
109+ message = error_message ,
110+ )
111+ exception .text = EnvelopeFactory (error = error_model ).as_text ()
112+
113+ return exception
114+
115+
116+ def _handle_http_successful (
117+ request : web .Request , exception : web .HTTPSuccessful
118+ ) -> web .HTTPSuccessful :
119+ """Handle successful HTTP responses, ensuring they're properly enveloped."""
120+ assert request # nosec
121+
122+ exception .content_type = MIMETYPE_APPLICATION_JSON
123+ if exception .reason :
124+ exception .set_status (
125+ exception .status , safe_status_message (message = exception .reason )
126+ )
127+
128+ if exception .text :
129+ payload = json_loads (exception .text )
130+ if not is_enveloped_from_map (payload ):
131+ payload = wrap_as_envelope (data = payload )
132+ exception .text = json_dumps (payload )
133+
134+ return exception
135+
136+
137+ def _handle_exception_as_http_error (
138+ request : web .Request ,
139+ exception : Exception ,
140+ status_code : int ,
141+ * ,
142+ skip_internal_error_details : bool ,
143+ ) -> HTTPError :
144+ """
145+ Generic handler for exceptions that map to specific HTTP status codes.
146+ Converts the status code to the appropriate HTTP error class and creates a response.
147+ """
148+ assert request # nosec
149+
150+ http_error_cls = get_http_error_class_or_none (status_code )
151+ if http_error_cls is None :
152+ msg = (
153+ f"No HTTP error class found for status code { status_code } , falling back to 500" ,
73154 )
74- raise http_error
155+ raise ValueError (msg )
156+
157+ return create_http_error (
158+ exception ,
159+ f"{ exception } " ,
160+ http_error_cls ,
161+ skip_internal_error_details = skip_internal_error_details ,
162+ )
163+
164+
165+ def error_middleware_factory (api_version : str ) -> Middleware :
166+ _is_prod : bool = is_production_environ ()
75167
76168 @web .middleware
77- async def _middleware_handler (request : web .Request , handler : Handler ): # noqa: C901
169+ async def _middleware_handler (request : web .Request , handler : Handler ):
78170 """
79171 Ensure all error raised are properly enveloped and json responses
80172 """
81173 if not is_api_request (request , api_version ):
82174 return await handler (request )
83175
84- # FIXME: review when to send info to client and when not!
85176 try :
86- return await handler (request )
177+ try :
178+ result = await handler (request )
87179
88- except web .HTTPError as err :
89-
90- err .content_type = MIMETYPE_APPLICATION_JSON
91- if err .reason :
92- err .set_status (err .status , safe_status_message (message = err .reason ))
93-
94- if not err .text or not is_enveloped_from_text (err .text ):
95- error_message = err .text or err .reason or "Unexpected error"
96- error_model = ErrorGet (
97- errors = [
98- ErrorItemType .from_error (err ),
99- ],
100- status = err .status ,
101- logs = [
102- LogMessageType (message = error_message , level = "ERROR" ),
103- ],
104- message = error_message ,
180+ except web .HTTPError as exc : # 4XX and 5XX raised as exceptions
181+ result = _handle_http_error (request , exc )
182+
183+ except web .HTTPSuccessful as exc : # 2XX rased as exceptions
184+ result = _handle_http_successful (request , exc )
185+
186+ except web .HTTPRedirection as exc : # 3XX raised as exceptions
187+ result = exc
188+
189+ except NotImplementedError as exc :
190+ result = _handle_exception_as_http_error (
191+ request ,
192+ exc ,
193+ status .HTTP_501_NOT_IMPLEMENTED ,
194+ skip_internal_error_details = _is_prod ,
105195 )
106- err .text = EnvelopeFactory (error = error_model ).as_text ()
107-
108- raise
109-
110- except web .HTTPSuccessful as err :
111- err .content_type = MIMETYPE_APPLICATION_JSON
112- if err .reason :
113- err .set_status (err .status , safe_status_message (message = err .reason ))
114-
115- if err .text :
116- try :
117- payload = json_loads (err .text )
118- if not is_enveloped_from_map (payload ):
119- payload = wrap_as_envelope (data = payload )
120- err .text = json_dumps (payload )
121- except Exception as other_error : # pylint: disable=broad-except
122- _process_and_raise_unexpected_error (request , other_error )
123- raise
124-
125- except web .HTTPRedirection as err :
126- _logger .debug ("Redirected to %s" , err )
127- raise
128-
129- except NotImplementedError as err :
130- http_error = create_http_error (
131- err ,
132- f"{ err } " ,
133- web .HTTPNotImplemented ,
134- skip_internal_error_details = _is_prod ,
135- )
136- raise http_error from err
137-
138- except TimeoutError as err :
139- http_error = create_http_error (
140- err ,
141- f"{ err } " ,
142- web .HTTPGatewayTimeout ,
143- skip_internal_error_details = _is_prod ,
196+
197+ except TimeoutError as exc :
198+ result = _handle_exception_as_http_error (
199+ request ,
200+ exc ,
201+ status .HTTP_504_GATEWAY_TIMEOUT ,
202+ skip_internal_error_details = _is_prod ,
203+ )
204+
205+ except Exception as exc : # pylint: disable=broad-except
206+ #
207+ # Last resort for unexpected exceptions (including those raise by the exception handlers!)
208+ #
209+ result = _handle_unexpected_exception_as_500 (
210+ request , exc , skip_internal_error_details = _is_prod
144211 )
145- raise http_error from err
146212
147- except Exception as err : # pylint: disable=broad-except
148- _process_and_raise_unexpected_error (request , err )
213+ return result
149214
150215 # adds identifier (mostly for debugging)
151216 setattr ( # noqa: B010
0 commit comments