Skip to content

Commit c820e6b

Browse files
committed
Merge branch 'master' into 7795-add-osparc-trace-id-to-response-headers
2 parents 935ceb3 + 110179c commit c820e6b

File tree

75 files changed

+1051
-500
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+1051
-500
lines changed

api/tests/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
aiohappyeyeballs==2.4.6
1+
aiohappyeyeballs==2.6.1
22
# via aiohttp
3-
aiohttp==3.11.18
3+
aiohttp==3.12.7
44
# via
55
# -c ../../requirements/constraints.txt
66
# -r requirements.in

ci/helpers/requirements/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
aiohappyeyeballs==2.6.1
22
# via aiohttp
3-
aiohttp==3.11.18
3+
aiohttp==3.12.7
44
# via
55
# -c requirements/../../../requirements/constraints.txt
66
# -r requirements/requirements.in

packages/aws-library/requirements/_base.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ aiofiles==24.1.0
1616
# via
1717
# -r requirements/../../../packages/service-library/requirements/_base.in
1818
# aioboto3
19-
aiohappyeyeballs==2.4.6
19+
aiohappyeyeballs==2.6.1
2020
# via aiohttp
21-
aiohttp==3.11.18
21+
aiohttp==3.12.7
2222
# via
2323
# -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
2424
# -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt

packages/dask-task-models-library/src/dask_task_models_library/container_tasks/errors.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
""" Dask task exceptions
1+
"""Dask task exceptions"""
22

3-
"""
43
from common_library.errors_classes import OsparcErrorMixin
54

65

7-
class TaskValueError(OsparcErrorMixin, ValueError):
8-
...
6+
class TaskValueError(OsparcErrorMixin, ValueError): ...
97

108

119
class TaskCancelledError(OsparcErrorMixin, RuntimeError):
@@ -18,3 +16,12 @@ class ServiceRuntimeError(OsparcErrorMixin, RuntimeError):
1816
" running in container {container_id} failed with code"
1917
" {exit_code}. Last logs:\n{service_logs}"
2018
)
19+
20+
21+
class ServiceInputsUseFileToKeyMapButReceivesZipDataError(
22+
OsparcErrorMixin, RuntimeError
23+
):
24+
msg_template = (
25+
"The service {service_key}:{service_version} {input} uses a file-to-key {file_to_key_map} map but receives zip data instead. "
26+
"TIP: either pass a single file or zip file and remove the file-to-key map parameter."
27+
)

packages/notifications-library/requirements/_test.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
aiodocker==0.24.0
22
# via -r requirements/_test.in
3-
aiohappyeyeballs==2.4.6
3+
aiohappyeyeballs==2.6.1
44
# via aiohttp
5-
aiohttp==3.11.18
5+
aiohttp==3.12.7
66
# via
77
# -c requirements/../../../requirements/constraints.txt
88
# aiodocker

packages/service-library/requirements/_aiohttp.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
aiohappyeyeballs==2.4.6
1+
aiohappyeyeballs==2.6.1
22
# via aiohttp
3-
aiohttp==3.11.18
3+
aiohttp==3.12.7
44
# via -r requirements/_aiohttp.in
55
aiopg==1.4.0
66
# via -r requirements/_aiohttp.in

packages/service-library/requirements/_base.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ aiodocker==0.24.0
88
# via -r requirements/_base.in
99
aiofiles==24.1.0
1010
# via -r requirements/_base.in
11-
aiohappyeyeballs==2.4.6
11+
aiohappyeyeballs==2.6.1
1212
# via aiohttp
13-
aiohttp==3.11.18
13+
aiohttp==3.12.7
1414
# via
1515
# -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
1616
# -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt

packages/service-library/requirements/_test.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
aiohappyeyeballs==2.4.6
1+
aiohappyeyeballs==2.6.1
22
# via
33
# -c requirements/_aiohttp.txt
44
# -c requirements/_base.txt
55
# aiohttp
6-
aiohttp==3.11.18
6+
aiohttp==3.12.7
77
# via
88
# -c requirements/../../../requirements/constraints.txt
99
# -c requirements/_aiohttp.txt

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

Lines changed: 12 additions & 5 deletions
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)
@@ -176,10 +186,7 @@ async def _middleware_handler(
176186
return resp
177187

178188
if not isinstance(resp, StreamResponse):
179-
resp = create_data_response(
180-
data=resp,
181-
skip_internal_error_details=_is_prod,
182-
)
189+
resp = create_data_response(data=resp)
183190

184191
assert isinstance(resp, web.StreamResponse) # nosec
185192
return resp
Lines changed: 62 additions & 80 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"""
1+
from typing import Any, Final, TypedDict
22

3-
import inspect
4-
from typing import Any
5-
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
@@ -13,46 +10,71 @@
1310
from ..mimetype_constants import MIMETYPE_APPLICATION_JSON
1411
from ..rest_constants import RESPONSE_MODEL_POLICY
1512
from ..rest_responses import is_enveloped
16-
from ..status_codes_utils import get_code_description
13+
from ..status_codes_utils import get_code_description, is_error
14+
15+
16+
class EnvelopeDict(TypedDict):
17+
data: Any
18+
error: Any
1719

1820

1921
def wrap_as_envelope(
20-
data: Any = None,
21-
error: Any = None,
22-
) -> dict[str, Any]:
22+
data: Any | None = None,
23+
error: Any | None = None,
24+
) -> EnvelopeDict:
2325
return {"data": data, "error": error}
2426

2527

26-
# RESPONSES FACTORIES -------------------------------
28+
def create_data_response(data: Any, *, status: int = HTTP_200_OK) -> web.Response:
29+
"""Creates a JSON response with the given data and ensures it is wrapped in an envelope."""
2730

31+
assert ( # nosec
32+
is_error(status) is False
33+
), f"Expected a non-error status code, got {status=}"
2834

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+
enveloped_payload = wrap_as_envelope(data) if not is_enveloped(data) else data
36+
return web.json_response(enveloped_payload, dumps=json_dumps, status=status)
3537

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
38+
39+
MAX_STATUS_MESSAGE_LENGTH: Final[int] = 100
40+
41+
42+
def safe_status_message(
43+
message: str | None, max_length: int = MAX_STATUS_MESSAGE_LENGTH
44+
) -> str | None:
45+
"""
46+
Truncates a status-message (i.e. `reason` in HTTP errors) to a maximum length, replacing newlines with spaces.
47+
48+
If the message is longer than max_length, it will be truncated and "..." will be appended.
49+
50+
This prevents issues such as:
51+
- `aiohttp.http_exceptions.LineTooLong`: 400, message: Got more than 8190 bytes when reading Status line is too long.
52+
- Multiline not allowed in HTTP reason attribute (aiohttp now raises ValueError).
53+
54+
See:
55+
- When to use http status and/or text messages https://github.com/ITISFoundation/osparc-simcore/pull/7760
56+
- [RFC 9112, Section 4.1: HTTP/1.1 Message Syntax and Routing](https://datatracker.ietf.org/doc/html/rfc9112#section-4.1) (status line length limits)
57+
- [RFC 9110, Section 15.5: Reason Phrase](https://datatracker.ietf.org/doc/html/rfc9110#section-15.5) (reason phrase definition)
58+
"""
59+
assert max_length > 0 # nosec
60+
61+
if not message:
62+
return None
63+
64+
flat_message = message.replace("\n", " ")
65+
if len(flat_message) <= max_length:
66+
return flat_message
67+
68+
# Truncate and add ellipsis
69+
return flat_message[: max_length - 3] + "..."
4970

5071

5172
def create_http_error(
5273
errors: list[Exception] | Exception,
53-
reason: str | None = None,
74+
error_message: str | None = None,
5475
http_error_cls: type[HTTPError] = web.HTTPInternalServerError,
5576
*,
77+
status_reason: str | None = None,
5678
skip_internal_error_details: bool = False,
5779
error_code: ErrorCodeStr | None = None,
5880
) -> HTTPError:
@@ -64,14 +86,18 @@ def create_http_error(
6486
if not isinstance(errors, list):
6587
errors = [errors]
6688

67-
is_internal_error: bool = http_error_cls == web.HTTPInternalServerError
68-
default_message = reason or get_code_description(http_error_cls.status_code)
89+
is_internal_error = bool(http_error_cls == web.HTTPInternalServerError)
90+
91+
status_reason = status_reason or get_code_description(http_error_cls.status_code)
92+
error_message = error_message or get_code_description(http_error_cls.status_code)
93+
94+
assert len(status_reason) < MAX_STATUS_MESSAGE_LENGTH # nosec
6995

7096
if is_internal_error and skip_internal_error_details:
7197
error = ErrorGet.model_validate(
7298
{
7399
"status": http_error_cls.status_code,
74-
"message": default_message,
100+
"message": error_message,
75101
"support_id": error_code,
76102
}
77103
)
@@ -81,7 +107,7 @@ def create_http_error(
81107
{
82108
"errors": items, # NOTE: deprecated!
83109
"status": http_error_cls.status_code,
84-
"message": default_message,
110+
"message": error_message,
85111
"support_id": error_code,
86112
}
87113
)
@@ -92,7 +118,7 @@ def create_http_error(
92118
)
93119

94120
return http_error_cls(
95-
reason=safe_status_message(reason),
121+
reason=safe_status_message(status_reason),
96122
text=json_dumps(
97123
payload,
98124
),
@@ -110,47 +136,3 @@ def exception_to_response(exc: HTTPError) -> web.Response:
110136
reason=exc.reason,
111137
text=exc.text,
112138
)
113-
114-
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-
133-
def safe_status_message(message: str | None, max_length: int = 50) -> str | None:
134-
"""
135-
Truncates a status-message (i.e. `reason` in HTTP errors) to a maximum length, replacing newlines with spaces.
136-
137-
If the message is longer than max_length, it will be truncated and "..." will be appended.
138-
139-
This prevents issues such as:
140-
- `aiohttp.http_exceptions.LineTooLong`: 400, message: Got more than 8190 bytes when reading Status line is too long.
141-
- Multiline not allowed in HTTP reason attribute (aiohttp now raises ValueError).
142-
143-
See:
144-
- When to use http status and/or text messages https://github.com/ITISFoundation/osparc-simcore/pull/7760
145-
- [RFC 9112, Section 4.1: HTTP/1.1 Message Syntax and Routing](https://datatracker.ietf.org/doc/html/rfc9112#section-4.1) (status line length limits)
146-
- [RFC 9110, Section 15.5: Reason Phrase](https://datatracker.ietf.org/doc/html/rfc9110#section-15.5) (reason phrase definition)
147-
"""
148-
if not message:
149-
return None
150-
151-
flat_message = message.replace("\n", " ")
152-
if len(flat_message) <= max_length:
153-
return flat_message
154-
155-
# Truncate and add ellipsis
156-
return flat_message[: max_length - 3] + "..."

0 commit comments

Comments
 (0)