Skip to content

fix(Litestar): Apply failed_request_status_codes to exceptions raised in middleware #4074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0b64a40
Allow to override asgi capturers
vrslev Feb 19, 2025
4f1963c
Add test
vrslev Feb 19, 2025
169177f
Override request exception catcher for litestar
vrslev Feb 19, 2025
3950e2a
Fix formatting
vrslev Feb 19, 2025
e1cd18e
Remove debugging print
vrslev Feb 19, 2025
2b91e0d
Merge branch 'master' into fix-litestar-middleware-failed-request-sta…
vrslev Feb 19, 2025
6ac821b
Merge branch 'master' into fix-litestar-middleware-failed-request-sta…
vrslev Feb 21, 2025
0c2da8f
Merge branch 'master' into fix-litestar-middleware-failed-request-sta…
antonpirker Mar 27, 2025
204c8a7
Add missing import line for test_litestar.py
vrslev Apr 1, 2025
bace5ce
Refactor exception capture in SentryAsgiMiddleware to use a shared fu…
vrslev Apr 1, 2025
fb1274f
Amend comment to clarify exception handling behavior in SentryLitesta…
vrslev Apr 1, 2025
babfe27
Adjust documentation to clarify exception handling behavior in Sentry…
vrslev Apr 1, 2025
33b11cc
Added docstrings to lifespan and request exception capture methods in…
vrslev Apr 1, 2025
41b5fd5
Adjusts exception documentation in SentryAsgiMiddleware for clarity
vrslev Apr 1, 2025
3819888
Adjust comment for Litestar integration to remove unnecessary newline
vrslev Apr 1, 2025
5b27acd
Adjusts exception handling comments for clarity in SentryLitestarASGI…
vrslev Apr 1, 2025
53baddd
Merge branch 'master' into fix-litestar-middleware-failed-request-sta…
vrslev Apr 1, 2025
c1c318d
Merge branch 'master' into fix-litestar-middleware-failed-request-sta…
vrslev Apr 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,6 @@
TRANSACTION_STYLE_VALUES = ("endpoint", "url")


def _capture_exception(exc, mechanism_type="asgi"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep this function the same, as a standalone function instead of a method. I don't think it has to be modified to implement your changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

# type: (Any, str) -> None

event, hint = event_from_exception(
exc,
client_options=sentry_sdk.get_client().options,
mechanism={"type": mechanism_type, "handled": False},
)
sentry_sdk.capture_event(event, hint=hint)


def _looks_like_asgi3(app):
# type: (Any) -> bool
"""
Expand Down Expand Up @@ -145,6 +134,23 @@ def __init__(
else:
self.__call__ = self._run_asgi2

def _capture_exception(self, exc):
# type: (Exception) -> None
event, hint = event_from_exception(
exc,
client_options=sentry_sdk.get_client().options,
mechanism={"type": self.mechanism_type, "handled": False},
)
sentry_sdk.capture_event(event, hint=hint)

def _capture_lifespan_exception(self, exc):
# type: (Exception) -> None
return self._capture_exception(exc)

def _capture_request_exception(self, exc):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit unsure of the difference between these two functions. Please add docstrings to both to clarify the distinction

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

# type: (Exception) -> None
return self._capture_exception(exc)

def _run_asgi2(self, scope):
# type: (Any) -> Any
async def inner(receive, send):
Expand All @@ -158,7 +164,7 @@ async def _run_asgi3(self, scope, receive, send):
return await self._run_app(scope, receive, send, asgi_version=3)

async def _run_app(self, scope, receive, send, asgi_version):
# type: (Any, Any, Any, Any, int) -> Any
# type: (Any, Any, Any, int) -> Any
is_recursive_asgi_middleware = _asgi_middleware_applied.get(False)
is_lifespan = scope["type"] == "lifespan"
if is_recursive_asgi_middleware or is_lifespan:
Expand All @@ -169,7 +175,7 @@ async def _run_app(self, scope, receive, send, asgi_version):
return await self.app(scope, receive, send)

except Exception as exc:
_capture_exception(exc, mechanism_type=self.mechanism_type)
self._capture_lifespan_exception(exc)
raise exc from None

_asgi_middleware_applied.set(True)
Expand Down Expand Up @@ -255,7 +261,7 @@ async def _sentry_wrapped_send(event):
scope, receive, _sentry_wrapped_send
)
except Exception as exc:
_capture_exception(exc, mechanism_type=self.mechanism_type)
self._capture_request_exception(exc)
raise exc from None
finally:
_asgi_middleware_applied.set(False)
Expand Down
4 changes: 4 additions & 0 deletions sentry_sdk/integrations/litestar.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def __init__(self, app, span_origin=LitestarIntegration.origin):
span_origin=span_origin,
)

def _capture_request_exception(self, exc):
# type: (Exception) -> None
"""Ignore exceptions for requests here: we catch them in Litestar.after_exception handler."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am slightly unsure why this function is needed/how it relates to what you are trying to explain in this PR. Could you please explain in a bit more detail?

Also, I think you need a pass here:

Suggested change
def _capture_request_exception(self, exc):
# type: (Exception) -> None
"""Ignore exceptions for requests here: we catch them in Litestar.after_exception handler."""
def _capture_request_exception(self, exc):
# type: (Exception) -> None
"""Ignore exceptions for requests here: we catch them in Litestar.after_exception handler."""
pass

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, Litestar integration catches exceptions raised in middleware twice:

  1. In the custom ASGI handler.
  2. In the Litestar.after_exception handler, which is added in patch_app_init.

The custom ASGI handler doesn't filter exceptions using failed_request_status_codes, so all exceptions (including those not filtered by the expected status code) appear in Sentry.

Interestingly, this behavior doesn't apply to exceptions raised in request handlers because Litestar prepends the built-in exception handling middleware.


This leads us to the conclusion that the ASGI handler only catches exceptions raised in middleware or lifespan handlers. It's still valuable to catch exceptions from lifespan handlers.

In this case, we patch _capture_request_exception to prevent catching exceptions raised in middleware.



def patch_app_init():
# type: () -> None
Expand Down
36 changes: 34 additions & 2 deletions tests/integrations/litestar/test_litestar.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from litestar.middleware.rate_limit import RateLimitConfig
from litestar.middleware.session.server_side import ServerSideSessionConfig
from litestar.testing import TestClient

from tests.integrations.conftest import parametrize_test_configurable_status_codes


Expand Down Expand Up @@ -402,7 +401,7 @@ async def __call__(self, scope, receive, send):


@parametrize_test_configurable_status_codes
def test_configurable_status_codes(
def test_configurable_status_codes_handler(
sentry_init,
capture_events,
failed_request_status_codes,
Expand All @@ -427,3 +426,36 @@ async def error() -> None:
client.get("/error")

assert len(events) == int(expected_error)


@parametrize_test_configurable_status_codes
def test_configurable_status_codes_middleware(
sentry_init,
capture_events,
failed_request_status_codes,
status_code,
expected_error,
):
integration_kwargs = (
{"failed_request_status_codes": failed_request_status_codes}
if failed_request_status_codes is not None
else {}
)
sentry_init(integrations=[LitestarIntegration(**integration_kwargs)])

events = capture_events()

def create_raising_middleware(app):
async def raising_middleware(scope, receive, send):
raise HTTPException(status_code=status_code)

return raising_middleware

@get("/error")
async def error() -> None: ...

app = Litestar([error], middleware=[create_raising_middleware])
client = TestClient(app)
client.get("/error")

assert len(events) == int(expected_error)
Loading