Skip to content

Commit f6154b0

Browse files
committed
cleanup
1 parent 8190ce3 commit f6154b0

File tree

3 files changed

+68
-131
lines changed

3 files changed

+68
-131
lines changed

packages/models-library/src/models_library/rest_error.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
126126

127127

128128
class EnvelopedError(Envelope[None]):
129+
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/443
129130
error: ErrorGet
130131

131132
model_config = ConfigDict(

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

Lines changed: 37 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from typing import TypeVar
1414

1515
from aiohttp import web
16-
from common_library.json_serialization import json_dumps
16+
from common_library.user_messages import user_message
17+
from models_library.rest_error import EnvelopedError
1718
from pydantic import BaseModel, TypeAdapter, ValidationError
1819

1920
from ..mimetype_constants import MIMETYPE_APPLICATION_JSON
@@ -25,14 +26,13 @@
2526

2627
@contextmanager
2728
def handle_validation_as_http_error(
28-
*, error_msg_template: str, resource_name: str, use_error_v1: bool
29+
*, error_msg_template: str, resource_name: str
2930
) -> Iterator[None]:
3031
"""Context manager to handle ValidationError and reraise them as HTTPUnprocessableEntity error
3132
3233
Arguments:
3334
error_msg_template -- _description_
3435
resource_name --
35-
use_error_v1 -- If True, it uses new error response
3636
3737
Raises:
3838
web.HTTPUnprocessableEntity: (422) raised from a ValidationError
@@ -43,49 +43,37 @@ def handle_validation_as_http_error(
4343
yield
4444

4545
except ValidationError as err:
46-
details = [
46+
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/443
47+
_details = [
4748
{
48-
"loc": ".".join(map(str, e["loc"])),
49+
"loc": ".".join(map(str, e["loc"])), # e.g. "body.name"
4950
"msg": e["msg"],
5051
"type": e["type"],
5152
}
5253
for e in err.errors()
5354
]
54-
user_error_message = error_msg_template.format(
55-
failed=", ".join(d["loc"] for d in details)
56-
)
57-
58-
if use_error_v1:
59-
# NOTE: keeps backwards compatibility until ligher error response is implemented in the entire API
60-
# Implements servicelib.aiohttp.rest_responses.ErrorItemType
61-
errors = [
62-
{
63-
"code": e["type"],
64-
"message": e["msg"],
65-
"resource": resource_name,
66-
"field": e["loc"],
67-
}
68-
for e in details
69-
]
70-
error_json_str = json_dumps(
71-
{
72-
"error": {
73-
"status": status.HTTP_422_UNPROCESSABLE_ENTITY,
74-
"errors": errors,
75-
}
76-
}
77-
)
78-
else:
79-
# NEW proposed error for https://github.com/ITISFoundation/osparc-simcore/issues/443
80-
error_json_str = json_dumps(
81-
{
82-
"error": {
83-
"msg": user_error_message,
84-
"resource": resource_name, # optional
85-
"details": details, # optional
86-
}
55+
56+
errors_details = [
57+
{
58+
"code": e["type"],
59+
"message": e["msg"],
60+
"resource": resource_name,
61+
"field": e["loc"],
62+
}
63+
for e in _details
64+
]
65+
66+
error_json_str = EnvelopedError.model_validate(
67+
{
68+
"error": {
69+
"message": error_msg_template.format(
70+
failed=", ".join(e["field"] for e in errors_details)
71+
),
72+
"status": status.HTTP_422_UNPROCESSABLE_ENTITY,
73+
"errors": errors_details,
8774
}
88-
)
75+
}
76+
).model_dump_json(exclude_unset=True, exclude_none=True)
8977

9078
raise web.HTTPUnprocessableEntity( # 422
9179
text=error_json_str,
@@ -104,15 +92,10 @@ def handle_validation_as_http_error(
10492
def parse_request_path_parameters_as(
10593
parameters_schema_cls: type[ModelClass],
10694
request: web.Request,
107-
*,
108-
use_enveloped_error_v1: bool = True,
10995
) -> ModelClass:
11096
"""Parses path parameters from 'request' and validates against 'parameters_schema'
11197
11298
113-
Keyword Arguments:
114-
use_enveloped_error_v1 -- new enveloped error model (default: {True})
115-
11699
Raises:
117100
web.HTTPUnprocessableEntity: (422) if validation of parameters fail
118101
@@ -121,9 +104,10 @@ def parse_request_path_parameters_as(
121104
"""
122105

123106
with handle_validation_as_http_error(
124-
error_msg_template="Invalid parameter/s '{failed}' in request path",
107+
error_msg_template=user_message(
108+
"Invalid parameter/s '{failed}' in request path"
109+
),
125110
resource_name=request.rel_url.path,
126-
use_error_v1=use_enveloped_error_v1,
127111
):
128112
data = dict(request.match_info)
129113
return parameters_schema_cls.model_validate(data)
@@ -132,15 +116,10 @@ def parse_request_path_parameters_as(
132116
def parse_request_query_parameters_as(
133117
parameters_schema_cls: type[ModelClass],
134118
request: web.Request,
135-
*,
136-
use_enveloped_error_v1: bool = True,
137119
) -> ModelClass:
138120
"""Parses query parameters from 'request' and validates against 'parameters_schema'
139121
140122
141-
Keyword Arguments:
142-
use_enveloped_error_v1 -- new enveloped error model (default: {True})
143-
144123
Raises:
145124
web.HTTPUnprocessableEntity: (422) if validation of parameters fail
146125
@@ -149,9 +128,10 @@ def parse_request_query_parameters_as(
149128
"""
150129

151130
with handle_validation_as_http_error(
152-
error_msg_template="Invalid parameter/s '{failed}' in request query",
131+
error_msg_template=user_message(
132+
"Invalid parameter/s '{failed}' in request query"
133+
),
153134
resource_name=request.rel_url.path,
154-
use_error_v1=use_enveloped_error_v1,
155135
):
156136
# NOTE: Currently, this does not take into consideration cases where there are multiple
157137
# query parameters with the same key. However, we are not using such cases anywhere at the moment.
@@ -166,13 +146,12 @@ def parse_request_query_parameters_as(
166146
def parse_request_headers_as(
167147
parameters_schema_cls: type[ModelClass],
168148
request: web.Request,
169-
*,
170-
use_enveloped_error_v1: bool = True,
171149
) -> ModelClass:
172150
with handle_validation_as_http_error(
173-
error_msg_template="Invalid parameter/s '{failed}' in request headers",
151+
error_msg_template=user_message(
152+
"Invalid parameter/s '{failed}' in request headers"
153+
),
174154
resource_name=request.rel_url.path,
175-
use_error_v1=use_enveloped_error_v1,
176155
):
177156
data = dict(request.headers)
178157
return parameters_schema_cls.model_validate(data)
@@ -181,8 +160,6 @@ def parse_request_headers_as(
181160
async def parse_request_body_as(
182161
model_schema_cls: type[ModelOrListOrDictType],
183162
request: web.Request,
184-
*,
185-
use_enveloped_error_v1: bool = True,
186163
) -> ModelOrListOrDictType:
187164
"""Parses and validates request body against schema
188165
@@ -197,9 +174,8 @@ async def parse_request_body_as(
197174
Validated model of request body
198175
"""
199176
with handle_validation_as_http_error(
200-
error_msg_template="Invalid field/s '{failed}' in request body",
177+
error_msg_template=user_message("Invalid field/s '{failed}' in request body"),
201178
resource_name=request.rel_url.path,
202-
use_error_v1=use_enveloped_error_v1,
203179
):
204180
if not request.can_read_body:
205181
# requests w/o body e.g. when model-schema is fully optional

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

Lines changed: 30 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from common_library.json_serialization import json_dumps
1414
from faker import Faker
1515
from models_library.rest_base import RequestParameters, StrictRequestParameters
16+
from models_library.rest_error import EnvelopedError
1617
from models_library.rest_ordering import (
1718
OrderBy,
1819
OrderDirection,
@@ -116,18 +117,10 @@ async def _handler(request: web.Request) -> web.Response:
116117
{**dict(request.app), **dict(request)}
117118
)
118119

119-
path_params = parse_request_path_parameters_as(
120-
MyRequestPathParams, request, use_enveloped_error_v1=False
121-
)
122-
query_params = parse_request_query_parameters_as(
123-
MyRequestQueryParams, request, use_enveloped_error_v1=False
124-
)
125-
headers_params = parse_request_headers_as(
126-
MyRequestHeadersParams, request, use_enveloped_error_v1=False
127-
)
128-
body = await parse_request_body_as(
129-
MyBody, request, use_enveloped_error_v1=False
130-
)
120+
path_params = parse_request_path_parameters_as(MyRequestPathParams, request)
121+
query_params = parse_request_query_parameters_as(MyRequestQueryParams, request)
122+
headers_params = parse_request_headers_as(MyRequestHeadersParams, request)
123+
body = await parse_request_body_as(MyBody, request)
131124
# ---------------------------
132125

133126
return web.json_response(
@@ -230,19 +223,12 @@ async def test_parse_request_with_invalid_path_params(
230223
assert r.status == status.HTTP_422_UNPROCESSABLE_ENTITY, f"{await r.text()}"
231224

232225
response_body = await r.json()
233-
assert response_body["error"].pop("resource")
234-
assert response_body == {
235-
"error": {
236-
"msg": "Invalid parameter/s 'project_uuid' in request path",
237-
"details": [
238-
{
239-
"loc": "project_uuid",
240-
"msg": "Input should be a valid UUID, invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `i` at 1",
241-
"type": "uuid_parsing",
242-
}
243-
],
244-
}
245-
}
226+
227+
error_model = EnvelopedError.model_validate(response_body).error
228+
assert error_model.message == "Invalid parameter/s 'project_uuid' in request path"
229+
assert error_model.status == status.HTTP_422_UNPROCESSABLE_ENTITY
230+
assert error_model.errors[0].field == "project_uuid"
231+
assert error_model.errors[0].code == "uuid_parsing"
246232

247233

248234
async def test_parse_request_with_invalid_query_params(
@@ -261,19 +247,11 @@ async def test_parse_request_with_invalid_query_params(
261247
assert r.status == status.HTTP_422_UNPROCESSABLE_ENTITY, f"{await r.text()}"
262248

263249
response_body = await r.json()
264-
assert response_body["error"].pop("resource")
265-
assert response_body == {
266-
"error": {
267-
"msg": "Invalid parameter/s 'label' in request query",
268-
"details": [
269-
{
270-
"loc": "label",
271-
"msg": "Field required",
272-
"type": "missing",
273-
}
274-
],
275-
}
276-
}
250+
error_model = EnvelopedError.model_validate(response_body).error
251+
assert error_model.message == "Invalid parameter/s 'label' in request query"
252+
assert error_model.status == status.HTTP_422_UNPROCESSABLE_ENTITY
253+
assert error_model.errors[0].field == "label"
254+
assert error_model.errors[0].code == "missing"
277255

278256

279257
async def test_parse_request_with_invalid_body(
@@ -293,25 +271,11 @@ async def test_parse_request_with_invalid_body(
293271

294272
response_body = await r.json()
295273

296-
assert response_body["error"].pop("resource")
297-
298-
assert response_body == {
299-
"error": {
300-
"msg": "Invalid field/s 'x, z' in request body",
301-
"details": [
302-
{
303-
"loc": "x",
304-
"msg": "Field required",
305-
"type": "missing",
306-
},
307-
{
308-
"loc": "z",
309-
"msg": "Field required",
310-
"type": "missing",
311-
},
312-
],
313-
}
314-
}
274+
error_model = EnvelopedError.model_validate(response_body).error
275+
assert error_model.message == "Invalid field/s 'x, z' in request body"
276+
assert error_model.status == status.HTTP_422_UNPROCESSABLE_ENTITY
277+
assert error_model.errors[0].field == "x"
278+
assert error_model.errors[0].code == "missing"
315279

316280

317281
async def test_parse_request_with_invalid_json_body(
@@ -349,19 +313,15 @@ async def test_parse_request_with_invalid_headers_params(
349313
assert r.status == status.HTTP_422_UNPROCESSABLE_ENTITY, f"{await r.text()}"
350314

351315
response_body = await r.json()
352-
assert response_body["error"].pop("resource")
353-
assert response_body == {
354-
"error": {
355-
"msg": "Invalid parameter/s 'X-Simcore-User-Agent' in request headers",
356-
"details": [
357-
{
358-
"loc": "X-Simcore-User-Agent",
359-
"msg": "Field required",
360-
"type": "missing",
361-
}
362-
],
363-
}
364-
}
316+
317+
error_model = EnvelopedError.model_validate(response_body).error
318+
assert (
319+
error_model.message
320+
== "Invalid parameter/s 'X-Simcore-User-Agent' in request headers"
321+
)
322+
assert error_model.status == status.HTTP_422_UNPROCESSABLE_ENTITY
323+
assert error_model.errors[0].field == "X-Simcore-User-Agent"
324+
assert error_model.errors[0].code == "missing"
365325

366326

367327
def test_parse_request_query_parameters_as_with_order_by_query_models():

0 commit comments

Comments
 (0)