Skip to content

Commit 3577006

Browse files
committed
✨ Refactor error handling: improve status message handling in HTTP errors
1 parent 39f1b4f commit 3577006

File tree

4 files changed

+34
-56
lines changed

4 files changed

+34
-56
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
from ..mimetype_constants import MIMETYPE_APPLICATION_JSON
1919
from ..rest_responses import is_enveloped_from_map, is_enveloped_from_text
2020
from ..utils import is_production_environ
21-
from .rest_responses import create_data_response, create_http_error, wrap_as_envelope
21+
from .rest_responses import (
22+
create_data_response,
23+
create_http_error,
24+
safe_status_message,
25+
wrap_as_envelope,
26+
)
2227
from .rest_utils import EnvelopeFactory
2328
from .typing_extension import Handler, Middleware
2429

@@ -83,6 +88,8 @@ async def _middleware_handler(request: web.Request, handler: Handler): # noqa:
8388
except web.HTTPError as err:
8489

8590
err.content_type = MIMETYPE_APPLICATION_JSON
91+
if err.reason:
92+
err.set_status(err.status, safe_status_message(message=err.reason))
8693

8794
if not err.text or not is_enveloped_from_text(err.text):
8895
error_message = err.text or err.reason or "Unexpected error"
@@ -102,6 +109,9 @@ async def _middleware_handler(request: web.Request, handler: Handler): # noqa:
102109

103110
except web.HTTPSuccessful as err:
104111
err.content_type = MIMETYPE_APPLICATION_JSON
112+
if err.reason:
113+
err.set_status(err.status, safe_status_message(message=err.reason))
114+
105115
if err.text:
106116
try:
107117
payload = json_loads(err.text)

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

Lines changed: 14 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
"""Utils to check, convert and compose server responses for the RESTApi"""
2-
3-
import inspect
41
from typing import Any
52

6-
from aiohttp import web, web_exceptions
7-
from aiohttp.web_exceptions import HTTPError, HTTPException
3+
from aiohttp import web
4+
from aiohttp.web_exceptions import HTTPError
85
from common_library.error_codes import ErrorCodeStr
96
from common_library.json_serialization import json_dumps
107
from models_library.rest_error import ErrorGet, ErrorItemType
@@ -23,36 +20,17 @@ def wrap_as_envelope(
2320
return {"data": data, "error": error}
2421

2522

26-
# RESPONSES FACTORIES -------------------------------
27-
28-
29-
def create_data_response(
30-
data: Any, *, skip_internal_error_details=False, status=HTTP_200_OK
31-
) -> web.Response:
32-
response = None
33-
try:
34-
payload = wrap_as_envelope(data) if not is_enveloped(data) else data
35-
36-
response = web.json_response(payload, dumps=json_dumps, status=status)
37-
except (TypeError, ValueError) as err:
38-
response = exception_to_response(
39-
create_http_error(
40-
[
41-
err,
42-
],
43-
str(err),
44-
web.HTTPInternalServerError,
45-
skip_internal_error_details=skip_internal_error_details,
46-
)
47-
)
48-
return response
23+
def create_data_response(data: Any, *, status: int = HTTP_200_OK) -> web.Response:
24+
enveloped_payload = wrap_as_envelope(data) if not is_enveloped(data) else data
25+
return web.json_response(enveloped_payload, dumps=json_dumps, status=status)
4926

5027

5128
def create_http_error(
5229
errors: list[Exception] | Exception,
53-
reason: str | None = None,
30+
error_message: str | None = None,
5431
http_error_cls: type[HTTPError] = web.HTTPInternalServerError,
5532
*,
33+
status_reason: str | None = None,
5634
skip_internal_error_details: bool = False,
5735
error_code: ErrorCodeStr | None = None,
5836
) -> HTTPError:
@@ -64,14 +42,16 @@ def create_http_error(
6442
if not isinstance(errors, list):
6543
errors = [errors]
6644

67-
is_internal_error: bool = http_error_cls == web.HTTPInternalServerError
68-
default_message = reason or get_code_description(http_error_cls.status_code)
45+
is_internal_error = bool(http_error_cls == web.HTTPInternalServerError)
46+
47+
status_reason = status_reason or get_code_description(http_error_cls.status_code)
48+
error_message = error_message or status_reason
6949

7050
if is_internal_error and skip_internal_error_details:
7151
error = ErrorGet.model_validate(
7252
{
7353
"status": http_error_cls.status_code,
74-
"message": default_message,
54+
"message": error_message,
7555
"support_id": error_code,
7656
}
7757
)
@@ -81,7 +61,7 @@ def create_http_error(
8161
{
8262
"errors": items, # NOTE: deprecated!
8363
"status": http_error_cls.status_code,
84-
"message": default_message,
64+
"message": error_message,
8565
"support_id": error_code,
8666
}
8767
)
@@ -92,7 +72,7 @@ def create_http_error(
9272
)
9373

9474
return http_error_cls(
95-
reason=safe_status_message(reason),
75+
reason=safe_status_message(status_reason),
9676
text=json_dumps(
9777
payload,
9878
),
@@ -112,24 +92,6 @@ def exception_to_response(exc: HTTPError) -> web.Response:
11292
)
11393

11494

115-
# Inverse map from code to HTTPException classes
116-
def _collect_http_exceptions(exception_cls: type[HTTPException] = HTTPException):
117-
def _pred(obj) -> bool:
118-
return (
119-
inspect.isclass(obj)
120-
and issubclass(obj, exception_cls)
121-
and getattr(obj, "status_code", 0) > 0
122-
)
123-
124-
found: list[tuple[str, Any]] = inspect.getmembers(web_exceptions, _pred)
125-
assert found # nosec
126-
127-
http_statuses = {cls.status_code: cls for _, cls in found}
128-
assert len(http_statuses) == len(found), "No duplicates" # nosec
129-
130-
return http_statuses
131-
132-
13395
def safe_status_message(message: str | None, max_length: int = 50) -> str | None:
13496
"""
13597
Truncates a status-message (i.e. `reason` in HTTP errors) to a maximum length, replacing newlines with spaces.

packages/service-library/tests/aiohttp/test_rest_responses.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,14 @@ def test_collected_http_errors_map(status_code: int, http_error_cls: type[HTTPEr
6363
@pytest.mark.parametrize("error_code", [None, create_error_code(Exception("fake"))])
6464
def tests_exception_to_response(skip_details: bool, error_code: ErrorCodeStr | None):
6565

66-
expected_reason = "Something whent wrong !"
66+
expected_status_reason = "SHORT REASON"
67+
expected_error_message = "Something whent wrong !"
6768
expected_exceptions: list[Exception] = [RuntimeError("foo")]
6869

6970
http_error = create_http_error(
7071
errors=expected_exceptions,
71-
reason=expected_reason,
72+
error_message=expected_error_message,
73+
status_reason="SHORT REASON",
7274
http_error_cls=web.HTTPInternalServerError,
7375
skip_internal_error_details=skip_details,
7476
error_code=error_code,
@@ -94,7 +96,7 @@ def tests_exception_to_response(skip_details: bool, error_code: ErrorCodeStr | N
9496
# checks response model
9597
response_json = json.loads(response.text)
9698
assert response_json["data"] is None
97-
assert response_json["error"]["message"] == expected_reason
99+
assert response_json["error"]["message"] == expected_error_message
98100
assert response_json["error"]["supportId"] == error_code
99101
assert response_json["error"]["status"] == response.status
100102

services/web/server/src/simcore_service_webserver/director_v2/_controller/_rest_exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
async def _handler_director_service_error_as_503_or_4xx(
3131
request: web.Request, exception: Exception
3232
) -> web.Response:
33+
"""
34+
Handles DirectorV2ServiceError exceptions by returning a 503 Service Unavailable if it's a server error (5XX),
35+
or a 4XX client error if it's a client error (4XX).
36+
"""
3337
assert isinstance(exception, DirectorV2ServiceError) # nosec
3438
assert status_codes_utils.is_error(
3539
exception.status

0 commit comments

Comments
 (0)