Skip to content

Commit ce92bee

Browse files
authored
🐛♻️ webserver error handling: Fix incorrect error logging for web.HTTPCreated; cleanup success response handling (#7952)
1 parent 904b669 commit ce92bee

File tree

8 files changed

+49
-33
lines changed

8 files changed

+49
-33
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def handle_validation_as_http_error(
6767
}
6868
for e in details
6969
]
70-
error_str = json_dumps(
70+
error_json_str = json_dumps(
7171
{
7272
"error": {
7373
"status": status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -77,7 +77,7 @@ def handle_validation_as_http_error(
7777
)
7878
else:
7979
# NEW proposed error for https://github.com/ITISFoundation/osparc-simcore/issues/443
80-
error_str = json_dumps(
80+
error_json_str = json_dumps(
8181
{
8282
"error": {
8383
"msg": user_error_message,
@@ -88,7 +88,7 @@ def handle_validation_as_http_error(
8888
)
8989

9090
raise web.HTTPUnprocessableEntity( # 422
91-
text=error_str,
91+
text=error_json_str,
9292
content_type=MIMETYPE_APPLICATION_JSON,
9393
) from err
9494

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from ..logging_errors import create_troubleshootting_log_kwargs
2323
from ..mimetype_constants import MIMETYPE_APPLICATION_JSON
24-
from ..rest_responses import is_enveloped_from_map, is_enveloped_from_text
24+
from ..rest_responses import is_enveloped_from_text
2525
from ..status_codes_utils import get_code_description
2626
from . import status
2727
from .rest_responses import (
@@ -167,11 +167,10 @@ def _handle_http_successful(
167167
exception.status, safe_status_message(message=exception.reason)
168168
)
169169

170-
if exception.text:
171-
payload = json_loads(exception.text)
172-
if not is_enveloped_from_map(payload):
173-
payload = wrap_as_envelope(data=payload)
174-
exception.text = json_dumps(payload)
170+
if exception.text and not is_enveloped_from_text(exception.text):
171+
# Ensures that the response is enveloped
172+
data = json_loads(exception.text)
173+
exception.text = json_dumps({"data": data})
175174

176175
return exception
177176

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,19 @@ def create_http_error(
133133
)
134134

135135

136-
def exception_to_response(exc: HTTPError) -> web.Response:
136+
def exception_to_response(exception: HTTPError) -> web.Response:
137137
# Returning web.HTTPException is deprecated so here we have a converter to a response
138138
# so it can be used as
139139
# SEE https://github.com/aio-libs/aiohttp/issues/2415
140+
141+
if exception.reason:
142+
reason = safe_status_message(exception.reason)
143+
else:
144+
reason = get_code_description(exception.status)
145+
140146
return web.Response(
141-
status=exc.status,
142-
headers=exc.headers,
143-
reason=exc.reason,
144-
text=exc.text,
147+
status=exception.status,
148+
headers=exception.headers,
149+
reason=reason,
150+
text=exception.text,
145151
)

packages/service-library/src/servicelib/long_running_tasks/http_endpoint_responses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async def get_task_result(
4444
except Exception as exc:
4545
_logger.exception(
4646
**create_troubleshootting_log_kwargs(
47-
user_error_msg=f"{task_id=} raised an exception",
47+
user_error_msg=f"{task_id=} raised an exception while getting its result",
4848
error=exc,
4949
error_code=create_error_code(exc),
5050
error_context={"task_context": task_context, "task_id": task_id},

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ async def raise_success_with_text(_request: web.Request):
128128
# NOTE: explicitly NOT enveloped!
129129
raise web.HTTPOk(reason="I'm ok", text=json.dumps({"ok": True}))
130130

131+
@staticmethod
132+
async def raise_success_with_raw_text(_request: web.Request):
133+
raise web.HTTPOk(text="I'm ok") # NOT ALLOWED!
134+
131135

132136
@pytest.fixture
133137
async def client(
@@ -158,6 +162,10 @@ async def client(
158162
("/v1/raise_success", Handlers.raise_success),
159163
("/v1/raise_success_with_reason", Handlers.raise_success_with_reason),
160164
("/v1/raise_success_with_text", Handlers.raise_success_with_text),
165+
(
166+
"/v1/raise_success_with_raw_text",
167+
Handlers.raise_success_with_raw_text,
168+
),
161169
]
162170
]
163171
)
@@ -330,6 +338,7 @@ async def test_http_ok_with_text_is_enveloped(client: TestClient):
330338
"""Test that HTTPOk with text is properly enveloped."""
331339
response = await client.get("/v1/raise_success_with_text")
332340
assert response.status == status.HTTP_200_OK
341+
assert response.reason == "I'm ok"
333342

334343
# Should be enveloped
335344
payload = await response.json()
@@ -342,6 +351,11 @@ async def test_http_ok_with_text_is_enveloped(client: TestClient):
342351
assert data.get("ok") is True
343352

344353

354+
async def test_http_ok_with_raw_text_is_not_allowed(client: TestClient):
355+
response = await client.get("/v1/raise_success_with_raw_text")
356+
assert response.status == status.HTTP_500_INTERNAL_SERVER_ERROR
357+
358+
345359
async def test_exception_in_handler_returns_500(
346360
client: TestClient, mocker: MockerFixture
347361
):

services/web/server/src/simcore_service_webserver/exception_handling/_factory.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
from servicelib.aiohttp.rest_responses import safe_status_message
99
from servicelib.aiohttp.web_exceptions_extension import get_all_aiohttp_http_exceptions
1010
from servicelib.logging_errors import create_troubleshootting_log_kwargs
11-
from servicelib.status_codes_utils import is_5xx_server_error, is_error
11+
from servicelib.status_codes_utils import (
12+
get_code_display_name,
13+
is_5xx_server_error,
14+
is_error,
15+
)
1216

1317
from ._base import AiohttpExceptionHandler, ExceptionHandlersMap
1418

@@ -50,7 +54,7 @@ def create_error_response(error: ErrorGet, status_code: int) -> web.Response:
5054
return web.json_response(
5155
data={"error": error.model_dump(exclude_unset=True, mode="json")},
5256
dumps=json_dumps,
53-
reason=safe_status_message(error.message),
57+
reason=safe_status_message(get_code_display_name(status_code)),
5458
status=status_code,
5559
)
5660

services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
263263
simcore_user_agent: str,
264264
parent_project_uuid: ProjectID | None,
265265
parent_node_id: NodeID | None,
266-
) -> None:
266+
) -> web.HTTPCreated:
267267
"""Implements TaskProtocol for 'create_projects' handler
268268
269269
Arguments:
@@ -278,7 +278,6 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
278278
predefined_project -- project in request body
279279
280280
Raises:
281-
web.HTTPCreated: succeeded
282281
web.HTTPBadRequest:
283282
web.HTTPNotFound:
284283
web.HTTPUnauthorized:
@@ -481,7 +480,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
481480
**RESPONSE_MODEL_POLICY
482481
)
483482

484-
raise web.HTTPCreated(
483+
return web.HTTPCreated(
485484
text=json_dumps({"data": data}),
486485
content_type=MIMETYPE_APPLICATION_JSON,
487486
)

services/web/server/tests/unit/isolated/test_exception_handling_factory.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from aiohttp.test_utils import make_mocked_request
1414
from servicelib.aiohttp import status
1515
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
16+
from servicelib.status_codes_utils import get_code_display_name
1617
from simcore_service_webserver.errors import WebServerBaseError
1718
from simcore_service_webserver.exception_handling._base import (
1819
ExceptionHandlingContextManager,
@@ -28,16 +29,13 @@
2829
# Some custom errors in my service
2930

3031

31-
class BaseError(WebServerBaseError):
32-
...
32+
class BaseError(WebServerBaseError): ...
3333

3434

35-
class OneError(BaseError):
36-
...
35+
class OneError(BaseError): ...
3736

3837

39-
class OtherError(BaseError):
40-
...
38+
class OtherError(BaseError): ...
4139

4240

4341
@pytest.fixture
@@ -58,7 +56,7 @@ async def test_factory__create_exception_handler_from_http_error(
5856
response = await one_error_to_404(fake_request, caught)
5957
assert response.status == status.HTTP_404_NOT_FOUND
6058
assert response.text is not None
61-
assert "one error message" in response.reason
59+
assert response.reason == get_code_display_name(response.status)
6260
assert response.content_type == MIMETYPE_APPLICATION_JSON
6361

6462

@@ -82,9 +80,7 @@ async def test_handling_different_exceptions_with_context(
8280
response = cm.get_response_or_none()
8381
assert response is not None
8482
assert response.status == status.HTTP_400_BAD_REQUEST
85-
assert response.reason == exc_to_http_error_map[OneError].msg_template.format(
86-
code="WebServerBaseError.BaseError.OneError"
87-
)
83+
assert response.reason == get_code_display_name(response.status)
8884
assert not caplog.records
8985

9086
# unhandled -> reraises
@@ -103,9 +99,7 @@ async def test_handling_different_exceptions_with_context(
10399
response = cm.get_response_or_none()
104100
assert response is not None
105101
assert response.status == status.HTTP_500_INTERNAL_SERVER_ERROR
106-
assert response.reason == exc_to_http_error_map[OtherError].msg_template.format(
107-
code="WebServerBaseError.BaseError.OtherError"
108-
)
102+
assert response.reason == get_code_display_name(response.status)
109103
assert caplog.records, "Expected 5XX troubleshooting logged as error"
110104
assert caplog.records[0].levelno == logging.ERROR
111105

0 commit comments

Comments
 (0)