Skip to content

Commit dae0218

Browse files
vrslevantonpirkerszokeasaurusrex
authored
fix(Litestar): Apply failed_request_status_codes to exceptions raised in middleware (#4074)
This is a fix for #4021: exceptions raised in middleware were sent without taking into account `failed_request_status_codes` value. See the test case for an example. --------- Co-authored-by: Anton Pirker <[email protected]> Co-authored-by: Daniel Szoke <[email protected]>
1 parent 65d31af commit dae0218

File tree

3 files changed

+93
-4
lines changed

3 files changed

+93
-4
lines changed

sentry_sdk/integrations/asgi.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,22 @@ def __init__(
145145
else:
146146
self.__call__ = self._run_asgi2
147147

148+
def _capture_lifespan_exception(self, exc):
149+
# type: (Exception) -> None
150+
"""Capture exceptions raise in application lifespan handlers.
151+
152+
The separate function is needed to support overriding in derived integrations that use different catching mechanisms.
153+
"""
154+
return _capture_exception(exc=exc, mechanism_type=self.mechanism_type)
155+
156+
def _capture_request_exception(self, exc):
157+
# type: (Exception) -> None
158+
"""Capture exceptions raised in incoming request handlers.
159+
160+
The separate function is needed to support overriding in derived integrations that use different catching mechanisms.
161+
"""
162+
return _capture_exception(exc=exc, mechanism_type=self.mechanism_type)
163+
148164
def _run_asgi2(self, scope):
149165
# type: (Any) -> Any
150166
async def inner(receive, send):
@@ -158,7 +174,7 @@ async def _run_asgi3(self, scope, receive, send):
158174
return await self._run_app(scope, receive, send, asgi_version=3)
159175

160176
async def _run_app(self, scope, receive, send, asgi_version):
161-
# type: (Any, Any, Any, Any, int) -> Any
177+
# type: (Any, Any, Any, int) -> Any
162178
is_recursive_asgi_middleware = _asgi_middleware_applied.get(False)
163179
is_lifespan = scope["type"] == "lifespan"
164180
if is_recursive_asgi_middleware or is_lifespan:
@@ -169,7 +185,7 @@ async def _run_app(self, scope, receive, send, asgi_version):
169185
return await self.app(scope, receive, send)
170186

171187
except Exception as exc:
172-
_capture_exception(exc, mechanism_type=self.mechanism_type)
188+
self._capture_lifespan_exception(exc)
173189
raise exc from None
174190

175191
_asgi_middleware_applied.set(True)
@@ -256,7 +272,7 @@ async def _sentry_wrapped_send(event):
256272
scope, receive, _sentry_wrapped_send
257273
)
258274
except Exception as exc:
259-
_capture_exception(exc, mechanism_type=self.mechanism_type)
275+
self._capture_request_exception(exc)
260276
raise exc from None
261277
finally:
262278
_asgi_middleware_applied.set(False)

sentry_sdk/integrations/litestar.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ def __init__(self, app, span_origin=LitestarIntegration.origin):
8787
span_origin=span_origin,
8888
)
8989

90+
def _capture_request_exception(self, exc):
91+
# type: (Exception) -> None
92+
"""Avoid catching exceptions from request handlers.
93+
94+
Those exceptions are already handled in Litestar.after_exception handler.
95+
We still catch exceptions from application lifespan handlers.
96+
"""
97+
pass
98+
9099

91100
def patch_app_init():
92101
# type: () -> None

tests/integrations/litestar/test_litestar.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ async def __call__(self, scope, receive, send):
402402

403403

404404
@parametrize_test_configurable_status_codes
405-
def test_configurable_status_codes(
405+
def test_configurable_status_codes_handler(
406406
sentry_init,
407407
capture_events,
408408
failed_request_status_codes,
@@ -427,3 +427,67 @@ async def error() -> None:
427427
client.get("/error")
428428

429429
assert len(events) == int(expected_error)
430+
431+
432+
@parametrize_test_configurable_status_codes
433+
def test_configurable_status_codes_middleware(
434+
sentry_init,
435+
capture_events,
436+
failed_request_status_codes,
437+
status_code,
438+
expected_error,
439+
):
440+
integration_kwargs = (
441+
{"failed_request_status_codes": failed_request_status_codes}
442+
if failed_request_status_codes is not None
443+
else {}
444+
)
445+
sentry_init(integrations=[LitestarIntegration(**integration_kwargs)])
446+
447+
events = capture_events()
448+
449+
def create_raising_middleware(app):
450+
async def raising_middleware(scope, receive, send):
451+
raise HTTPException(status_code=status_code)
452+
453+
return raising_middleware
454+
455+
@get("/error")
456+
async def error() -> None: ...
457+
458+
app = Litestar([error], middleware=[create_raising_middleware])
459+
client = TestClient(app)
460+
client.get("/error")
461+
462+
assert len(events) == int(expected_error)
463+
464+
465+
def test_catch_non_http_exceptions_in_middleware(
466+
sentry_init,
467+
capture_events,
468+
):
469+
sentry_init(integrations=[LitestarIntegration()])
470+
471+
events = capture_events()
472+
473+
def create_raising_middleware(app):
474+
async def raising_middleware(scope, receive, send):
475+
raise RuntimeError("Too Hot")
476+
477+
return raising_middleware
478+
479+
@get("/error")
480+
async def error() -> None: ...
481+
482+
app = Litestar([error], middleware=[create_raising_middleware])
483+
client = TestClient(app)
484+
485+
try:
486+
client.get("/error")
487+
except RuntimeError:
488+
pass
489+
490+
assert len(events) == 1
491+
event_exception = events[0]["exception"]["values"][0]
492+
assert event_exception["type"] == "RuntimeError"
493+
assert event_exception["value"] == "Too Hot"

0 commit comments

Comments
 (0)