Skip to content

Commit 639d9cf

Browse files
committed
updates error handling on server side
1 parent e75c4fd commit 639d9cf

File tree

3 files changed

+100
-124
lines changed

3 files changed

+100
-124
lines changed

services/web/server/src/simcore_service_webserver/invitations/_client.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import contextlib
2+
import functools
23
import logging
3-
from contextlib import contextmanager
4+
from collections.abc import Callable
45
from dataclasses import dataclass
56

67
from aiohttp import BasicAuth, ClientResponseError, ClientSession, web
@@ -16,7 +17,6 @@
1617
from yarl import URL
1718

1819
from .._constants import APP_SETTINGS_KEY
19-
from ._client import InvitationsServiceApi
2020
from .errors import (
2121
InvalidInvitationError,
2222
InvitationsError,
@@ -27,38 +27,48 @@
2727
_logger = logging.getLogger(__name__)
2828

2929

30-
@contextmanager
31-
def _handle_exceptions_as_invitations_errors():
32-
try:
33-
yield # API function calls happen
34-
35-
except ClientResponseError as err:
36-
# check possible errors
37-
if err.status == status.HTTP_422_UNPROCESSABLE_ENTITY:
38-
raise InvalidInvitationError(
39-
invitations_api_response={
40-
"err": err,
41-
"status": err.status,
42-
"message": err.message,
43-
"url": err.request_info.real_url,
44-
},
45-
) from err
30+
def _handle_exceptions_as_invitations_errors(member_func: Callable):
31+
@functools.wraps(member_func)
32+
async def _wrapper(*args, **kwargs):
33+
"""
34+
Raises:
35+
InvalidInvitationError:
36+
InvitationsServiceUnavailableError:
37+
"""
38+
try:
39+
40+
return await member_func(*args, **kwargs)
41+
42+
except ClientResponseError as err:
4643

47-
assert err.status >= status.HTTP_400_BAD_REQUEST # nosec
44+
if err.status == status.HTTP_422_UNPROCESSABLE_ENTITY:
45+
raise InvalidInvitationError(
46+
api_funcname=member_func.__name__,
47+
status=err.status,
48+
message=err.message,
49+
url=err.request_info.real_url,
50+
) from err
4851

49-
# any other error status code
50-
raise InvitationsServiceUnavailableError(
51-
client_response_error=err,
52-
) from err
52+
assert err.status >= status.HTTP_400_BAD_REQUEST # nosec
5353

54-
except InvitationsError:
55-
# bypass: prevents that the Exceptions handler catches this exception
56-
raise
54+
# any other error status code
55+
raise InvitationsServiceUnavailableError(
56+
api_funcname=member_func.__name__,
57+
status=err.status,
58+
message=err.message,
59+
url=err.request_info.real_url,
60+
) from err
61+
62+
except InvitationsError:
63+
# bypass: prevents that the Exceptions handler catches this exception
64+
raise
65+
66+
except Exception as err:
67+
raise InvitationsServiceUnavailableError(
68+
unexpected_error=err,
69+
) from err
5770

58-
except Exception as err:
59-
raise InvitationsServiceUnavailableError(
60-
unexpected_error=err,
61-
) from err
71+
return _wrapper
6272

6373

6474
@dataclass(frozen=True)
@@ -116,6 +126,7 @@ async def ping(self) -> bool:
116126
# service API
117127
#
118128

129+
@_handle_exceptions_as_invitations_errors
119130
async def extract_invitation(
120131
self, invitation_url: AnyHttpUrl
121132
) -> ApiInvitationContent:
@@ -125,6 +136,7 @@ async def extract_invitation(
125136
)
126137
return ApiInvitationContent.model_validate(await response.json())
127138

139+
@_handle_exceptions_as_invitations_errors
128140
async def generate_invitation(
129141
self, params: ApiInvitationInputs
130142
) -> ApiInvitationContentAndLink:
Lines changed: 56 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,28 @@
11
import logging
2-
from contextlib import contextmanager
32
from typing import Final
43

5-
from aiohttp import ClientResponseError, web
4+
from aiohttp import web
65
from models_library.api_schemas_invitations.invitations import (
76
ApiInvitationContent,
87
ApiInvitationContentAndLink,
98
ApiInvitationInputs,
109
)
1110
from models_library.emails import LowerCaseEmailStr
1211
from pydantic import AnyHttpUrl, TypeAdapter, ValidationError
13-
from servicelib.aiohttp import status
1412

1513
from ..groups.api import is_user_by_email_in_group
1614
from ..products.api import Product
17-
from ._client import InvitationsServiceApi, get_invitations_service_api
15+
from ._client import get_invitations_service_api
1816
from .errors import (
1917
MSG_INVALID_INVITATION_URL,
2018
MSG_INVITATION_ALREADY_USED,
2119
InvalidInvitationError,
22-
InvitationsError,
2320
InvitationsServiceUnavailableError,
2421
)
2522

2623
_logger = logging.getLogger(__name__)
2724

2825

29-
@contextmanager
30-
def _handle_exceptions_as_invitations_errors():
31-
try:
32-
yield # API function calls happen
33-
34-
except ClientResponseError as err:
35-
# check possible errors
36-
if err.status == status.HTTP_422_UNPROCESSABLE_ENTITY:
37-
raise InvalidInvitationError(
38-
invitations_api_response={
39-
"err": err,
40-
"status": err.status,
41-
"message": err.message,
42-
"url": err.request_info.real_url,
43-
},
44-
) from err
45-
46-
assert err.status >= status.HTTP_400_BAD_REQUEST # nosec
47-
48-
# any other error status code
49-
raise InvitationsServiceUnavailableError(
50-
client_response_error=err,
51-
) from err
52-
53-
except InvitationsError:
54-
# bypass: prevents that the Exceptions handler catches this exception
55-
raise
56-
57-
except Exception as err:
58-
raise InvitationsServiceUnavailableError(
59-
unexpected_error=err,
60-
) from err
61-
62-
6326
#
6427
# API plugin CALLS
6528
#
@@ -81,53 +44,50 @@ async def validate_invitation_url(
8144
) -> ApiInvitationContent:
8245
"""Validates invitation and associated email/user and returns content upon success
8346
84-
raises InvitationsError
47+
Raises:
48+
InvitationsError
49+
InvalidInvitationError:
50+
InvitationsServiceUnavailableError:
8551
"""
8652
if current_product.group_id is None:
8753
raise InvitationsServiceUnavailableError(
8854
reason="Current product is not configured for invitations"
8955
)
9056

91-
invitations_service: InvitationsServiceApi = get_invitations_service_api(app=app)
92-
93-
with _handle_exceptions_as_invitations_errors():
94-
try:
95-
valid_url = TypeAdapter(AnyHttpUrl).validate_python(invitation_url)
96-
except ValidationError as err:
97-
raise InvalidInvitationError(reason=MSG_INVALID_INVITATION_URL) from err
98-
99-
# check with service
100-
invitation = await invitations_service.extract_invitation(
101-
invitation_url=valid_url
57+
try:
58+
valid_url = TypeAdapter(AnyHttpUrl).validate_python(invitation_url)
59+
except ValidationError as err:
60+
raise InvalidInvitationError(reason=MSG_INVALID_INVITATION_URL) from err
61+
62+
# check with service
63+
invitation = await get_invitations_service_api(app=app).extract_invitation(
64+
invitation_url=valid_url
65+
)
66+
67+
# check email
68+
if invitation.guest.lower() != guest_email.lower():
69+
raise InvalidInvitationError(
70+
reason="This invitation was issued for a different email"
10271
)
10372

104-
# check email
105-
if invitation.guest.lower() != guest_email.lower():
106-
raise InvalidInvitationError(
107-
reason="This invitation was issued for a different email"
108-
)
109-
110-
# check product
111-
assert current_product.group_id is not None # nosec
112-
if (
113-
invitation.product is not None
114-
and invitation.product != current_product.name
115-
):
116-
raise InvalidInvitationError(
117-
reason="This invitation was issued for a different product. "
118-
f"Got '{invitation.product}', expected '{current_product.name}'"
119-
)
120-
121-
# check invitation used
122-
assert invitation.product == current_product.name # nosec
123-
is_user_registered_in_product: bool = await is_user_by_email_in_group(
124-
app,
125-
user_email=LowerCaseEmailStr(invitation.guest),
126-
group_id=current_product.group_id,
73+
# check product
74+
assert current_product.group_id is not None # nosec
75+
if invitation.product is not None and invitation.product != current_product.name:
76+
raise InvalidInvitationError(
77+
reason="This invitation was issued for a different product. "
78+
f"Got '{invitation.product}', expected '{current_product.name}'"
12779
)
128-
if is_user_registered_in_product:
129-
# NOTE: a user might be already registered but the invitation is for another product
130-
raise InvalidInvitationError(reason=MSG_INVITATION_ALREADY_USED)
80+
81+
# check invitation used
82+
assert invitation.product == current_product.name # nosec
83+
is_user_registered_in_product: bool = await is_user_by_email_in_group(
84+
app,
85+
user_email=LowerCaseEmailStr(invitation.guest),
86+
group_id=current_product.group_id,
87+
)
88+
if is_user_registered_in_product:
89+
# NOTE: a user might be already registered but the invitation is for another product
90+
raise InvalidInvitationError(reason=MSG_INVITATION_ALREADY_USED)
13191

13292
return invitation
13393

@@ -137,25 +97,29 @@ async def extract_invitation(
13797
) -> ApiInvitationContent:
13898
"""Validates invitation and returns content without checking associated user
13999
140-
raises InvitationsError
100+
Raises:
101+
InvitationsError
102+
InvalidInvitationError:
103+
InvitationsServiceUnavailableError:
141104
"""
142-
invitations_service: InvitationsServiceApi = get_invitations_service_api(app=app)
143-
144-
with _handle_exceptions_as_invitations_errors():
145-
try:
146-
valid_url = TypeAdapter(AnyHttpUrl).validate_python(invitation_url)
147-
except ValidationError as err:
148-
raise InvalidInvitationError(reason=MSG_INVALID_INVITATION_URL) from err
105+
try:
106+
valid_url = TypeAdapter(AnyHttpUrl).validate_python(invitation_url)
107+
except ValidationError as err:
108+
raise InvalidInvitationError(reason=MSG_INVALID_INVITATION_URL) from err
149109

150-
# check with service
151-
return await invitations_service.extract_invitation(invitation_url=valid_url)
110+
# check with service
111+
return await get_invitations_service_api(app=app).extract_invitation(
112+
invitation_url=valid_url
113+
)
152114

153115

154116
async def generate_invitation(
155117
app: web.Application, params: ApiInvitationInputs
156118
) -> ApiInvitationContentAndLink:
157-
invitations_service: InvitationsServiceApi = get_invitations_service_api(app=app)
158-
159-
with _handle_exceptions_as_invitations_errors():
160-
# check with service
161-
return await invitations_service.generate_invitation(params)
119+
"""
120+
Raises:
121+
InvitationsError
122+
InvalidInvitationError:
123+
InvitationsServiceUnavailableError:
124+
"""
125+
return await get_invitations_service_api(app=app).generate_invitation(params)

services/web/server/tests/unit/with_dbs/03/invitations/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def mock_invitations_service_http_api(
116116
# extract
117117
assert "/v1/invitations:extract" in oas["paths"]
118118

119-
def _extract(url, **kwargs):
119+
def _extract_cbk(url, **kwargs):
120120
fake_code = URL(URL(f'{kwargs["json"]["invitation_url"]}').fragment).query[
121121
"invitation"
122122
]
@@ -133,7 +133,7 @@ def _extract(url, **kwargs):
133133

134134
aioresponses_mocker.post(
135135
f"{base_url}/v1/invitations:extract",
136-
callback=_extract,
136+
callback=_extract_cbk,
137137
repeat=True, # NOTE: this can be used many times
138138
)
139139

0 commit comments

Comments
 (0)