Skip to content

Commit 3c20e92

Browse files
Add function error handler to api server 🎨 (#7810)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 2f0a89d commit 3c20e92

File tree

4 files changed

+64
-5
lines changed

4 files changed

+64
-5
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,84 +2,100 @@
22

33

44
class FunctionBaseError(OsparcErrorMixin, Exception):
5-
pass
5+
status_code: int
66

77

88
class FunctionJobReadAccessDeniedError(FunctionBaseError):
99
msg_template: str = (
1010
"Function job {function_job_id} read access denied for user {user_id}"
1111
)
12+
status_code: int = 403 # Forbidden
1213

1314

1415
class FunctionIDNotFoundError(FunctionBaseError):
1516
msg_template: str = "Function {function_id} not found"
17+
status_code: int = 404 # Not Found
1618

1719

1820
class FunctionJobIDNotFoundError(FunctionBaseError):
1921
msg_template: str = "Function job {function_job_id} not found"
22+
status_code: int = 404 # Not Found
2023

2124

2225
class FunctionInputsValidationError(FunctionBaseError):
2326
msg_template: str = "Function inputs validation failed: {error}"
27+
status_code: int = 422 # Unprocessable Entity
2428

2529

2630
class FunctionReadAccessDeniedError(FunctionBaseError):
2731
msg_template: str = "Function {function_id} read access denied for user {user_id}"
32+
status_code: int = 403 # Forbidden
2833

2934

3035
class FunctionJobCollectionIDNotFoundError(FunctionBaseError):
3136
msg_template: str = "Function job collection {function_job_collection_id} not found"
37+
status_code: int = 404 # Not Found
3238

3339

3440
class UnsupportedFunctionClassError(FunctionBaseError):
3541
msg_template: str = "Function class {function_class} is not supported"
42+
status_code: int = 400 # Bad Request
3643

3744

3845
class UnsupportedFunctionJobClassError(FunctionBaseError):
3946
msg_template: str = "Function job class {function_job_class} is not supported"
47+
status_code: int = 400 # Bad Request
4048

4149

4250
class UnsupportedFunctionFunctionJobClassCombinationError(FunctionBaseError):
4351
msg_template: str = (
4452
"Function class {function_class} and function job class {function_job_class} combination is not supported"
4553
)
54+
status_code: int = 400 # Bad Request
4655

4756

4857
class FunctionJobCollectionReadAccessDeniedError(FunctionBaseError):
4958
msg_template: str = (
5059
"Function job collection {function_job_collection_id} read access denied for user {user_id}"
5160
)
61+
status_code: int = 403 # Forbidden
5262

5363

5464
class FunctionWriteAccessDeniedError(FunctionBaseError):
5565
msg_template: str = "Function {function_id} write access denied for user {user_id}"
66+
status_code: int = 403 # Forbidden
5667

5768

5869
class FunctionJobWriteAccessDeniedError(FunctionBaseError):
5970
msg_template: str = (
6071
"Function job {function_job_id} write access denied for user {user_id}"
6172
)
73+
status_code: int = 403 # Forbidden
6274

6375

6476
class FunctionJobCollectionWriteAccessDeniedError(FunctionBaseError):
6577
msg_template: str = (
6678
"Function job collection {function_job_collection_id} write access denied for user {user_id}"
6779
)
80+
status_code: int = 403 # Forbidden
6881

6982

7083
class FunctionExecuteAccessDeniedError(FunctionBaseError):
7184
msg_template: str = (
7285
"Function {function_id} execute access denied for user {user_id}"
7386
)
87+
status_code: int = 403 # Forbidden
7488

7589

7690
class FunctionJobExecuteAccessDeniedError(FunctionBaseError):
7791
msg_template: str = (
7892
"Function job {function_job_id} execute access denied for user {user_id}"
7993
)
94+
status_code: int = 403 # Forbidden
8095

8196

8297
class FunctionJobCollectionExecuteAccessDeniedError(FunctionBaseError):
8398
msg_template: str = (
8499
"Function job collection {function_job_collection_id} execute access denied for user {user_id}"
85100
)
101+
status_code: int = 403 # Forbidden

services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi import FastAPI
22
from fastapi.exceptions import RequestValidationError
33
from httpx import HTTPError as HttpxException
4+
from models_library.functions_errors import FunctionBaseError
45
from starlette import status
56
from starlette.exceptions import HTTPException
67

@@ -9,6 +10,7 @@
910
from ..custom_errors import CustomBaseError
1011
from ..log_streaming_errors import LogStreamingBaseError
1112
from ._custom_errors import custom_error_handler
13+
from ._handler_function_errors import function_error_handler
1214
from ._handlers_backend_errors import backend_error_handler
1315
from ._handlers_factory import make_handler_for_exception
1416
from ._http_exceptions import http_exception_handler
@@ -24,6 +26,7 @@ def setup(app: FastAPI, *, is_debug: bool = False):
2426
app.add_exception_handler(LogStreamingBaseError, log_handling_error_handler)
2527
app.add_exception_handler(CustomBaseError, custom_error_handler)
2628
app.add_exception_handler(BaseBackEndError, backend_error_handler)
29+
app.add_exception_handler(FunctionBaseError, function_error_handler)
2730

2831
# SEE https://docs.python.org/3/library/exceptions.html#exception-hierarchy
2932
app.add_exception_handler(
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from fastapi import Request
2+
from models_library.functions_errors import FunctionBaseError
3+
4+
from ._utils import create_error_json_response
5+
6+
7+
async def function_error_handler(request: Request, exc: Exception):
8+
assert request # nosec
9+
assert isinstance(exc, FunctionBaseError)
10+
11+
return create_error_json_response(f"{exc}", status_code=exc.status_code)

services/api-server/tests/unit/api_functions/test_api_routers_functions.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from uuid import uuid4
88

99
import httpx
10-
import pytest
1110
from httpx import AsyncClient
1211
from models_library.api_schemas_webserver.functions import (
1312
FunctionJobCollection,
@@ -17,7 +16,10 @@
1716
RegisteredProjectFunction,
1817
RegisteredProjectFunctionJob,
1918
)
20-
from models_library.functions_errors import FunctionIDNotFoundError
19+
from models_library.functions_errors import (
20+
FunctionIDNotFoundError,
21+
FunctionReadAccessDeniedError,
22+
)
2123
from models_library.rest_pagination import PageMetaInfoLimitOffset
2224
from servicelib.aiohttp import status
2325
from simcore_service_api_server._meta import API_VTAG
@@ -92,8 +94,35 @@ async def test_get_function_not_found(
9294
None,
9395
FunctionIDNotFoundError(function_id=non_existent_function_id),
9496
)
95-
with pytest.raises(FunctionIDNotFoundError):
96-
await client.get(f"{API_VTAG}/functions/{non_existent_function_id}", auth=auth)
97+
response = await client.get(
98+
f"{API_VTAG}/functions/{non_existent_function_id}", auth=auth
99+
)
100+
assert response.status_code == status.HTTP_404_NOT_FOUND
101+
102+
103+
async def test_get_function_read_access_denied(
104+
client: AsyncClient,
105+
mock_handler_in_functions_rpc_interface: Callable[
106+
[str, Any, Exception | None], None
107+
],
108+
mock_registered_function: RegisteredProjectFunction,
109+
auth: httpx.BasicAuth,
110+
) -> None:
111+
unauthorized_user_id = "unauthorized user"
112+
mock_handler_in_functions_rpc_interface(
113+
"get_function",
114+
None,
115+
FunctionReadAccessDeniedError(
116+
function_id=mock_registered_function.uid, user_id=unauthorized_user_id
117+
),
118+
)
119+
response = await client.get(
120+
f"{API_VTAG}/functions/{mock_registered_function.uid}", auth=auth
121+
)
122+
assert response.status_code == status.HTTP_403_FORBIDDEN
123+
assert response.json()["errors"][0] == (
124+
f"Function {mock_registered_function.uid} read access denied for user {unauthorized_user_id}"
125+
)
97126

98127

99128
async def test_list_functions(

0 commit comments

Comments
 (0)