Skip to content

Commit ceec907

Browse files
committed
cleanup
1 parent 7fa2ec7 commit ceec907

File tree

2 files changed

+75
-27
lines changed

2 files changed

+75
-27
lines changed

services/web/server/src/simcore_service_webserver/exception_handling_base.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ def _sort_exceptions_by_specificity(
4444

4545

4646
class ExceptionHandlingContextManager(AbstractAsyncContextManager):
47-
"""Essentially a dynamic try-except context manager to
48-
handle exceptions raised by a web handler, i.e.
47+
"""
48+
A dynamic try-except context manager for handling exceptions in web handlers.
49+
Maps exception types to corresponding handlers, allowing structured error management, i.e.
50+
essentially something like
4951
```
5052
try:
5153
@@ -55,10 +57,10 @@ class ExceptionHandlingContextManager(AbstractAsyncContextManager):
5557
resp = await exc_handler1(request)
5658
except exc_type2 as exc1:
5759
resp = await exc_handler2(request)
60+
# etc
5861
59-
# and so on ... as in `exception_handlers_map[exc_type] == exc_handler`
6062
```
61-
63+
and `exception_handlers_map` defines the mapping of exception types (`exc_type*`) to their handlers (`exc_handler*`).
6264
"""
6365

6466
def __init__(
@@ -113,15 +115,19 @@ async def __aexit__(
113115
return False # reraise
114116

115117
def get_response(self) -> web.Response | None:
118+
"""
119+
Returns the response generated by the exception handler, if an exception was handled. Otherwise None
120+
"""
116121
return self._response
117122

118123

119124
def exception_handling_decorator(
120125
exception_handlers_map: dict[type[BaseException], AiohttpExceptionHandler]
121126
):
122-
"""Creates a decorator to handle all registered exceptions raised in a given route handler
127+
"""Creates a decorator to manage exceptions raised in a given route handler.
128+
Ensures consistent exception management across decorated handlers.
123129
124-
SEE usage example in test_exception_handling
130+
SEE examples test_exception_handling
125131
"""
126132

127133
def _decorator(handler: WebHandler):
@@ -146,8 +152,9 @@ async def _wrapper(request: web.Request):
146152
def exception_handling_middleware(
147153
exception_handlers_map: dict[type[BaseException], AiohttpExceptionHandler]
148154
) -> WebMiddleware:
149-
"""
150-
Creates a middleware to handle all registered exceptions raised in any app's route
155+
"""Constructs middleware to handle exceptions raised across app routes
156+
157+
SEE examples test_exception_handling
151158
"""
152159
_handle_excs = exception_handling_decorator(
153160
exception_handlers_map=exception_handlers_map

services/web/server/tests/unit/isolated/test_exception_handling.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from aiohttp import web
1212
from models_library.rest_error import ErrorGet
1313
from servicelib.aiohttp import status
14+
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON, MIMETYPE_TEXT_PLAIN
1415
from simcore_service_webserver.exception_handling import (
1516
ExceptionHandlersMap,
1617
HttpErrorInfo,
@@ -20,6 +21,11 @@
2021
from simcore_service_webserver.exception_handling_base import (
2122
exception_handling_middleware,
2223
)
24+
from simcore_service_webserver.exception_handling_factory import (
25+
create_http_error_exception_handlers_map,
26+
)
27+
28+
from services.web.server.tests.conftest import TestClient
2329

2430

2531
@pytest.fixture
@@ -29,9 +35,9 @@ def exception_handlers_map(build_method: str) -> ExceptionHandlersMap:
2935
"""
3036
exception_handlers_map: ExceptionHandlersMap = {}
3137

32-
if build_method == "custom":
38+
if build_method == "function":
3339

34-
async def _value_error_as_422(
40+
async def _value_error_as_422_func(
3541
request: web.Request, exception: BaseException
3642
) -> web.Response:
3743
# custom exception handler
@@ -40,7 +46,7 @@ async def _value_error_as_422(
4046
)
4147

4248
exception_handlers_map = {
43-
ValueError: _value_error_as_422,
49+
ValueError: _value_error_as_422_func,
4450
}
4551

4652
elif build_method == "http_map":
@@ -57,7 +63,7 @@ async def _value_error_as_422(
5763
return exception_handlers_map
5864

5965

60-
@pytest.mark.parametrize("build_method", ["custom", "http_map"])
66+
@pytest.mark.parametrize("build_method", ["function", "http_map"])
6167
async def test_handling_exceptions_decorating_a_route(
6268
aiohttp_client: Callable,
6369
exception_handlers_map: ExceptionHandlersMap,
@@ -70,10 +76,11 @@ async def test_handling_exceptions_decorating_a_route(
7076
# adding new routes
7177
routes = web.RouteTableDef()
7278

73-
@routes.get("/{what}")
79+
@routes.post("/{what}")
7480
@exc_handling # < ----- 2. using decorator
7581
async def _handler(request: web.Request):
76-
match request.match_info["what"]:
82+
what = request.match_info["what"]
83+
match what:
7784
case "ValueError":
7885
raise ValueError # handled
7986
case "IndexError":
@@ -86,49 +93,48 @@ async def _handler(request: web.Request):
8693
# but if it is so ...
8794
raise web.HTTPOk # not-handled
8895

89-
return web.Response()
96+
return web.Response(text=what)
9097

9198
app = web.Application()
9299
app.add_routes(routes)
93100

94101
# 3. testing from the client side
95-
client = await aiohttp_client(app)
102+
client: TestClient = await aiohttp_client(app)
96103

97104
# success
98-
resp = await client.get("/ok")
105+
resp = await client.post("/ok")
99106
assert resp.status == status.HTTP_200_OK
100107

101108
# handled non-HTTPException exception
102-
resp = await client.get("/ValueError")
109+
resp = await client.post("/ValueError")
103110
assert resp.status == status.HTTP_422_UNPROCESSABLE_ENTITY
104111
if build_method == "http_map":
105112
body = await resp.json()
106113
error = ErrorGet.model_validate(body["error"])
107114
assert error.message == f"{build_method=}"
108115

109116
# undhandled non-HTTPException
110-
resp = await client.get("/IndexError")
117+
resp = await client.post("/IndexError")
111118
assert resp.status == status.HTTP_500_INTERNAL_SERVER_ERROR
112119

113120
# undhandled HTTPError
114-
resp = await client.get("/HTTPConflict")
121+
resp = await client.post("/HTTPConflict")
115122
assert resp.status == status.HTTP_409_CONFLICT
116123

117124
# undhandled HTTPSuccess
118-
resp = await client.get("/HTTPOk")
125+
resp = await client.post("/HTTPOk")
119126
assert resp.status == status.HTTP_200_OK
120127

121128

122-
@pytest.mark.parametrize("build_method", ["custom", "http_map"])
129+
@pytest.mark.parametrize("build_method", ["function", "http_map"])
123130
async def test_handling_exceptions_with_middelware(
124131
aiohttp_client: Callable,
125132
exception_handlers_map: ExceptionHandlersMap,
126133
build_method: str,
127134
):
128-
# adding new routes
129135
routes = web.RouteTableDef()
130136

131-
@routes.get("/{what}") # NO decorantor now
137+
@routes.post("/{what}") # NO decorantor now
132138
async def _handler(request: web.Request):
133139
match request.match_info["what"]:
134140
case "ValueError":
@@ -143,16 +149,51 @@ async def _handler(request: web.Request):
143149
app.middlewares.append(exc_handling)
144150

145151
# 2. testing from the client side
146-
client = await aiohttp_client(app)
152+
client: TestClient = await aiohttp_client(app)
147153

148154
# success
149-
resp = await client.get("/ok")
155+
resp = await client.post("/ok")
150156
assert resp.status == status.HTTP_200_OK
151157

152158
# handled non-HTTPException exception
153-
resp = await client.get("/ValueError")
159+
resp = await client.post("/ValueError")
154160
assert resp.status == status.HTTP_422_UNPROCESSABLE_ENTITY
155161
if build_method == "http_map":
156162
body = await resp.json()
157163
error = ErrorGet.model_validate(body["error"])
158164
assert error.message == f"{build_method=}"
165+
166+
167+
@pytest.mark.parametrize("with_middleware", [True, False])
168+
async def test_raising_aiohttp_http_errors(
169+
aiohttp_client: Callable, with_middleware: bool
170+
):
171+
routes = web.RouteTableDef()
172+
173+
@routes.post("/raise-http-error")
174+
async def _handler1(request: web.Request):
175+
# 1. raises aiohttp.web_exceptions.HttpError
176+
raise web.HTTPConflict
177+
178+
app = web.Application()
179+
app.add_routes(routes)
180+
181+
# 2. create & install middleware handlers for ALL http (optional)
182+
if with_middleware:
183+
exc_handling = exception_handling_middleware(
184+
exception_handlers_map=create_http_error_exception_handlers_map()
185+
)
186+
app.middlewares.append(exc_handling)
187+
188+
# 3. testing from the client side
189+
client: TestClient = await aiohttp_client(app)
190+
191+
resp = await client.post("/raise-http-error")
192+
assert resp.status == status.HTTP_409_CONFLICT
193+
194+
if with_middleware:
195+
assert resp.content_type == MIMETYPE_APPLICATION_JSON
196+
ErrorGet.model_construct((await resp.json())["error"])
197+
else:
198+
# default
199+
assert resp.content_type == MIMETYPE_TEXT_PLAIN

0 commit comments

Comments
 (0)