Skip to content

Commit 5b5c3e2

Browse files
committed
fixes raised HTTPSuccessfule and added more tests
1 parent b362599 commit 5b5c3e2

File tree

3 files changed

+120
-25
lines changed

3 files changed

+120
-25
lines changed

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

+31-19
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
SEE https://gist.github.com/amitripshtos/854da3f4217e3441e8fceea85b0cbd91
44
"""
55
import asyncio
6-
import json
76
import logging
87
from collections.abc import Awaitable, Callable
98
from typing import Any, Union
@@ -45,17 +44,21 @@ async def _handle_http_error(
4544
"""
4645
assert request # nosec
4746
assert err.reason # nosec
47+
assert str(err) == err.reason # nosec
4848

49-
err.content_type = MIMETYPE_APPLICATION_JSON
50-
if not err.empty_body and (not err.text or not is_enveloped_from_text(err.text)):
51-
error_body = ResponseErrorBody(
52-
message=err.reason,
53-
# FIXME: these below are not really necessary!
54-
status=err.status,
55-
errors=[ErrorDetail.from_exception(err)],
56-
logs=[LogMessage(message=err.reason, level="ERROR")],
57-
)
58-
err.text = EnvelopeFactory(error=error_body).as_text()
49+
if not err.empty_body:
50+
# By default exists if class method empty_body==False
51+
assert err.text # nosec
52+
53+
if err.text and not is_enveloped_from_text(err.text):
54+
error_body = ResponseErrorBody(
55+
message=err.reason, # we do not like default text=`{status}: {reason}`
56+
status=err.status,
57+
errors=[ErrorDetail.from_exception(err)],
58+
logs=[LogMessage(message=err.reason, level="ERROR")],
59+
)
60+
err.text = EnvelopeFactory(error=error_body).as_text()
61+
err.content_type = MIMETYPE_APPLICATION_JSON
5962

6063
return err
6164

@@ -69,16 +72,25 @@ async def _handle_http_successful(
6972
"""
7073
assert request # nosec
7174
assert err.reason # nosec
72-
err.content_type = MIMETYPE_APPLICATION_JSON
75+
assert str(err) == err.reason # nosec
76+
7377
if not err.empty_body:
74-
# NOTE: These are scenarios created by a lack of
75-
# consistency on how we respond in the request handlers.
76-
# This overhead can be avoided by having a more strict
77-
# response policy.
78-
if not err.text and err.reason:
78+
# By default exists if class method empty_body==False
79+
assert err.text # nosec
80+
81+
if err.text and not is_enveloped_from_text(err.text):
82+
# NOTE:
83+
# - aiohttp defaults `text={status}: {reason}` if not explictly defined and reason defaults
84+
# in http.HTTPStatus().phrase if not explicitly defined
85+
# - These are scenarios created by a lack of
86+
# consistency on how we respond in the request handlers.
87+
# This overhead can be avoided by having a more strict
88+
# response policy.
89+
# - Moreover there is an *concerning* asymmetry on how these responses are handled
90+
# depending whether the are returned or raised!!!!
7991
err.text = json_dumps({"data": err.reason})
80-
elif err.text and not is_enveloped_from_text(err.text):
81-
err.text = json_dumps({"data": json.loads(err.text)})
92+
err.content_type = MIMETYPE_APPLICATION_JSON
93+
8294
return err
8395

8496

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

+87-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import asyncio
77
import json
88
import logging
9+
from collections.abc import Callable
910
from dataclasses import dataclass
1011
from http import HTTPStatus
1112
from typing import Any
@@ -31,6 +32,7 @@
3132
)
3233
from servicelib.error_codes import parse_error_code
3334
from servicelib.json_serialization import json_dumps
35+
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
3436
from servicelib.status_codes_utils import (
3537
get_http_status_codes,
3638
is_2xx_success,
@@ -157,9 +159,33 @@ async def raise_exception(cls, request: web.Request):
157159
case _: # unexpected
158160
raise SomeUnexpectedError(cls.EXPECTED_RAISE_UNEXPECTED_REASON)
159161

162+
@staticmethod
163+
async def raise_error(request: web.Request):
164+
raise web.HTTPNotFound
165+
166+
@staticmethod
167+
async def raise_error_with_reason(request: web.Request):
168+
raise web.HTTPNotFound(reason="I did not find it")
169+
170+
@staticmethod
171+
async def raise_success(request: web.Request):
172+
raise web.HTTPOk
173+
174+
@staticmethod
175+
async def raise_success_with_reason(request: web.Request):
176+
raise web.HTTPOk(reason="I'm ok")
177+
178+
@staticmethod
179+
async def raise_success_with_text(request: web.Request):
180+
# NOTE: explicitly NOT enveloped!
181+
raise web.HTTPOk(reason="I'm ok", text=json.dumps({"ok": True}))
182+
160183

161184
@pytest.fixture
162-
def client(event_loop, aiohttp_client):
185+
def client(
186+
event_loop: asyncio.AbstractEventLoop,
187+
aiohttp_client: Callable,
188+
):
163189
app = web.Application()
164190

165191
# routes
@@ -176,7 +202,13 @@ def client(event_loop, aiohttp_client):
176202
("/v1/number", Handlers.get_number),
177203
("/v1/mixed", Handlers.get_mixed),
178204
("/v1/get_http_response", Handlers.get_http_response),
205+
# custom use cases
179206
("/v1/raise_exception", Handlers.raise_exception),
207+
("/v1/raise_error", Handlers.raise_error),
208+
("/v1/raise_error_with_reason", Handlers.raise_error_with_reason),
209+
("/v1/raise_success", Handlers.raise_success),
210+
("/v1/raise_success_with_reason", Handlers.raise_success_with_reason),
211+
("/v1/raise_success_with_text", Handlers.raise_success_with_text),
180212
]
181213
]
182214
)
@@ -292,13 +324,14 @@ async def test_fails_with_http_successful(client: TestClient, status_code: int):
292324
assert response.reason == Handlers.EXPECTED_HTTP_RESPONSE_REASON.format(status_code)
293325

294326
# NOTE: non-json response are sometimes necessary mostly on redirects
295-
# NOTE: this is how aiohptt defaults text using status and reason when empty_body is not expected
327+
# NOTE: this is how aiohttp defaults text using status and reason when empty_body is not expected
296328
expected = (
297329
""
298330
if all_aiohttp_http_exceptions[status_code].empty_body
299331
else f"{response.status}: {response.reason}"
300332
)
301333
assert await response.text() == expected
334+
# NOTE there is an concerning asymmetry between returning and raising web.HTTPSuccessful!!!
302335

303336

304337
@pytest.mark.parametrize(
@@ -408,3 +441,55 @@ async def test_aiohttp_exceptions_construction_policies(client: TestClient):
408441
text = await response.text()
409442
assert err.text == f"{err.status}: {err.reason}"
410443
print(text)
444+
445+
446+
async def test_raise_error(client: TestClient):
447+
# w/o reason
448+
resp1 = await client.get("/v1/raise_error")
449+
assert resp1.status == status.HTTP_404_NOT_FOUND
450+
assert resp1.content_type == MIMETYPE_APPLICATION_JSON
451+
assert resp1.reason == HTTPStatus(resp1.status).phrase
452+
453+
body = await resp1.json()
454+
assert body["error"]["message"] == resp1.reason
455+
456+
# without
457+
resp2 = await client.get("/v1/raise_error_with_reason")
458+
assert resp2.status == resp1.status
459+
assert resp2.content_type == resp1.content_type
460+
assert resp2.reason != resp1.reason
461+
462+
body = await resp2.json()
463+
assert body["error"]["message"] == resp2.reason
464+
465+
466+
async def test_raise_success(client: TestClient):
467+
# w/o reason
468+
resp_default = await client.get("/v1/raise_success")
469+
assert resp_default.status == status.HTTP_200_OK
470+
assert resp_default.content_type == MIMETYPE_APPLICATION_JSON
471+
assert resp_default.reason == HTTPStatus(resp_default.status).phrase
472+
473+
body = await resp_default.json()
474+
assert body["data"] == resp_default.reason
475+
476+
# without
477+
resp2 = await client.get("/v1/raise_success_with_reason")
478+
assert resp2.status == resp_default.status
479+
assert resp2.content_type == resp_default.content_type
480+
assert resp2.reason != resp_default.reason
481+
482+
body = await resp2.json()
483+
assert body["data"] == resp2.reason
484+
485+
# with text
486+
# NOTE: in this case, when we enforce text, then `reason` does not reach front-end anymore!
487+
resp3 = await client.get("/v1/raise_success_with_text")
488+
assert resp3.status == resp_default.status
489+
assert resp3.content_type == resp_default.content_type
490+
assert resp3.reason != resp_default.reason
491+
492+
body = await resp3.json()
493+
# explicitly NOT enveloped
494+
assert "data" not in body
495+
assert body == {"ok": True}

services/storage/src/simcore_service_storage/handlers_simcore_s3.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
import logging
32
from typing import NoReturn, cast
43

@@ -7,6 +6,7 @@
76
from models_library.api_schemas_storage import FileMetaDataGet, FoldersBody
87
from models_library.projects import ProjectID
98
from models_library.utils.fastapi_encoders import jsonable_encoder
9+
from servicelib.aiohttp import status
1010
from servicelib.aiohttp.long_running_tasks.server import (
1111
TaskProgress,
1212
start_long_running_task,
@@ -71,9 +71,7 @@ async def _copy_folders_from_project(
7171
task_progress=task_progress,
7272
)
7373

74-
raise web.HTTPCreated(
75-
text=json.dumps(body.destination), content_type=MIMETYPE_APPLICATION_JSON
76-
)
74+
return web.json_response({"data": body.destination}, status=status.HTTP_201_CREATED)
7775

7876

7977
@routes.post(f"/{api_vtag}/simcore-s3/folders", name="copy_folders_from_project")

0 commit comments

Comments
 (0)