Skip to content

Commit 2f4cdfb

Browse files
committed
🐛 Fix: improve error handling in middleware functions
1 parent 69b6a91 commit 2f4cdfb

File tree

2 files changed

+53
-47
lines changed

2 files changed

+53
-47
lines changed

packages/service-library/src/servicelib/aiohttp/rest_middlewares.py

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ def is_api_request(request: web.Request, api_version: str) -> bool:
4242
return bool(request.path.startswith(base_path))
4343

4444

45-
def _handle_unexpected_error(
46-
request: web.BaseRequest, exception: Exception, *, skip_internal_error_details: bool
45+
def _handle_unexpected_exception_as_500(
46+
request: web.BaseRequest,
47+
exception: Exception,
48+
*,
49+
skip_internal_error_details: bool,
4750
) -> web.HTTPInternalServerError:
4851
"""Process unexpected exceptions and return them as HTTP errors with proper formatting."""
4952
error_code = create_error_code(exception)
@@ -102,97 +105,97 @@ def _handle_http_error(
102105
def _handle_http_successful(
103106
request: web.Request,
104107
exception: web.HTTPSuccessful,
105-
*,
106-
skip_internal_error_details: bool,
107108
) -> web.HTTPSuccessful:
108109
"""Handle successful HTTP responses, ensuring they're properly enveloped."""
110+
assert request # nosec
111+
109112
exception.content_type = MIMETYPE_APPLICATION_JSON
110113
if exception.reason:
111114
exception.set_status(
112115
exception.status, safe_status_message(message=exception.reason)
113116
)
114117

115118
if exception.text:
116-
try:
117-
payload = json_loads(exception.text)
118-
if not is_enveloped_from_map(payload):
119-
payload = wrap_as_envelope(data=payload)
120-
exception.text = json_dumps(payload)
121-
except Exception as other_error: # pylint: disable=broad-except
122-
return _handle_unexpected_error(
123-
request,
124-
other_error,
125-
skip_internal_error_details=skip_internal_error_details,
126-
)
119+
payload = json_loads(exception.text)
120+
if not is_enveloped_from_map(payload):
121+
payload = wrap_as_envelope(data=payload)
122+
exception.text = json_dumps(payload)
127123

128124
return exception
129125

130126

131-
def _handle_not_implemented(
127+
def _handle_not_implemented_error(
132128
request: web.Request,
133129
exception: NotImplementedError,
134130
*,
135131
skip_internal_error_details: bool,
136132
) -> web.HTTPNotImplemented:
137133
"""Handle NotImplementedError by converting to appropriate HTTP error."""
138-
http_error = create_http_error(
134+
assert request # nosec
135+
136+
return create_http_error(
139137
exception,
140138
f"{exception}",
141139
web.HTTPNotImplemented,
142140
skip_internal_error_details=skip_internal_error_details,
143141
)
144-
return http_error
145142

146143

147-
def _handle_timeout(
148-
request: web.Request, exception: TimeoutError, *, skip_internal_error_details: bool
144+
def _handle_timeout_error(
145+
request: web.Request,
146+
exception: TimeoutError,
147+
*,
148+
skip_internal_error_details: bool,
149149
) -> web.HTTPGatewayTimeout:
150150
"""Handle TimeoutError by converting to appropriate HTTP error."""
151-
http_error = create_http_error(
151+
assert request # nosec
152+
return create_http_error(
152153
exception,
153154
f"{exception}",
154155
web.HTTPGatewayTimeout,
155156
skip_internal_error_details=skip_internal_error_details,
156157
)
157-
return http_error
158158

159159

160-
def error_middleware_factory( # noqa: C901
161-
api_version: str,
162-
) -> Middleware:
160+
def error_middleware_factory(api_version: str) -> Middleware:
163161
_is_prod: bool = is_production_environ()
164162

165163
@web.middleware
166-
async def _middleware_handler(request: web.Request, handler: Handler): # noqa: C901
164+
async def _middleware_handler(request: web.Request, handler: Handler):
167165
"""
168166
Ensure all error raised are properly enveloped and json responses
169167
"""
170168
if not is_api_request(request, api_version):
171169
return await handler(request)
172170

173-
# FIXME: review when to send info to client and when not!
174171
try:
175-
result = await handler(request)
176-
except web.HTTPError as exc:
177-
result = _handle_http_error(request, exc)
178-
except web.HTTPSuccessful as exc:
179-
result = _handle_http_successful(
180-
request, exc, skip_internal_error_details=_is_prod
181-
)
182-
except web.HTTPRedirection as exc:
183-
_logger.debug("Redirected to %s", exc)
184-
raise # We still raise redirection exceptions directly
172+
try:
173+
result = await handler(request)
185174

186-
except NotImplementedError as exc:
187-
result = _handle_not_implemented(
188-
request, exc, skip_internal_error_details=_is_prod
189-
)
175+
except web.HTTPError as exc: # 4XX and 5XX raised as exceptions
176+
result = _handle_http_error(request, exc)
177+
178+
except web.HTTPSuccessful as exc: # 2XX rased as exceptions
179+
result = _handle_http_successful(request, exc)
180+
181+
except web.HTTPRedirection as exc: # 3XX raised as exceptions
182+
_logger.debug("Redirected to %s", exc)
183+
result = exc
190184

191-
except TimeoutError as exc:
192-
result = _handle_timeout(request, exc, skip_internal_error_details=_is_prod)
185+
except NotImplementedError as exc:
186+
result = _handle_not_implemented_error(
187+
request, exc, skip_internal_error_details=_is_prod
188+
)
189+
except TimeoutError as exc:
190+
result = _handle_timeout_error(
191+
request, exc, skip_internal_error_details=_is_prod
192+
)
193193

194194
except Exception as exc: # pylint: disable=broad-except
195-
result = _handle_unexpected_error(
195+
#
196+
# Last resort for unexpected exceptions (including those in handlers!)
197+
#
198+
result = _handle_unexpected_exception_as_500(
196199
request, exc, skip_internal_error_details=_is_prod
197200
)
198201

packages/service-library/src/servicelib/aiohttp/rest_responses.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Final, TypedDict
1+
from typing import Any, Final, TypedDict, TypeVar
22

33
from aiohttp import web
44
from aiohttp.web_exceptions import HTTPError
@@ -69,15 +69,18 @@ def safe_status_message(
6969
return flat_message[: max_length - 3] + "..."
7070

7171

72+
T_HTTPError = TypeVar("T_HTTPError", bound=HTTPError)
73+
74+
7275
def create_http_error(
7376
errors: list[Exception] | Exception,
7477
error_message: str | None = None,
75-
http_error_cls: type[HTTPError] = web.HTTPInternalServerError,
78+
http_error_cls: type[T_HTTPError] = web.HTTPInternalServerError,
7679
*,
7780
status_reason: str | None = None,
7881
skip_internal_error_details: bool = False,
7982
error_code: ErrorCodeStr | None = None,
80-
) -> HTTPError:
83+
) -> T_HTTPError:
8184
"""
8285
- Response body conforms OAS schema model
8386
- Can skip internal details when 500 status e.g. to avoid transmitting server

0 commit comments

Comments
 (0)