From b5484902d7fc51e61ae9d3eabbb57614692e826c Mon Sep 17 00:00:00 2001 From: Johnny Deuss Date: Mon, 16 Jan 2023 19:38:04 +0000 Subject: [PATCH 1/4] Fix middleware being patched multiple times when using FastAPI --- sentry_sdk/integrations/starlette.py | 77 +++++++++++++++------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 155c840461..c4eed3366b 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -167,44 +167,47 @@ def patch_exception_middleware(middleware_class): """ old_middleware_init = middleware_class.__init__ - def _sentry_middleware_init(self, *args, **kwargs): - # type: (Any, Any, Any) -> None - old_middleware_init(self, *args, **kwargs) - - # Patch existing exception handlers - old_handlers = self._exception_handlers.copy() + not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) - async def _sentry_patched_exception_handler(self, *args, **kwargs): + if not_yet_patched: + def _sentry_middleware_init(self, *args, **kwargs): # type: (Any, Any, Any) -> None - exp = args[0] + old_middleware_init(self, *args, **kwargs) - is_http_server_error = ( - hasattr(exp, "status_code") and exp.status_code >= 500 - ) - if is_http_server_error: - _capture_exception(exp, handled=True) - - # Find a matching handler - old_handler = None - for cls in type(exp).__mro__: - if cls in old_handlers: - old_handler = old_handlers[cls] - break + # Patch existing exception handlers + old_handlers = self._exception_handlers.copy() + + async def _sentry_patched_exception_handler(self, *args, **kwargs): + # type: (Any, Any, Any) -> None + exp = args[0] - if old_handler is None: - return + is_http_server_error = ( + hasattr(exp, "status_code") and exp.status_code >= 500 + ) + if is_http_server_error: + _capture_exception(exp, handled=True) - if _is_async_callable(old_handler): - return await old_handler(self, *args, **kwargs) - else: - return old_handler(self, *args, **kwargs) + # Find a matching handler + old_handler = None + for cls in type(exp).__mro__: + if cls in old_handlers: + old_handler = old_handlers[cls] + break - for key in self._exception_handlers.keys(): - self._exception_handlers[key] = _sentry_patched_exception_handler + if old_handler is None: + return - middleware_class.__init__ = _sentry_middleware_init + if _is_async_callable(old_handler): + return await old_handler(self, *args, **kwargs) + else: + return old_handler(self, *args, **kwargs) - old_call = middleware_class.__call__ + for key in self._exception_handlers.keys(): + self._exception_handlers[key] = _sentry_patched_exception_handler + + middleware_class.__init__ = _sentry_middleware_init + + old_call = middleware_class.__call__ async def _sentry_exceptionmiddleware_call(self, scope, receive, send): # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None @@ -267,12 +270,16 @@ def patch_authentication_middleware(middleware_class): """ old_call = middleware_class.__call__ - async def _sentry_authenticationmiddleware_call(self, scope, receive, send): - # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None - await old_call(self, scope, receive, send) - _add_user_to_sentry_scope(scope) + not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call) + + if not_yet_patched: + + async def _sentry_authenticationmiddleware_call(self, scope, receive, send): + # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None + await old_call(self, scope, receive, send) + _add_user_to_sentry_scope(scope) - middleware_class.__call__ = _sentry_authenticationmiddleware_call + middleware_class.__call__ = _sentry_authenticationmiddleware_call def patch_middlewares(): From a880e0453b12c31d03a15a10f12b878ac48cfc93 Mon Sep 17 00:00:00 2001 From: Johnny Deuss Date: Thu, 19 Jan 2023 09:24:31 +0000 Subject: [PATCH 2/4] Place more code inside not_yet_patched block --- sentry_sdk/integrations/starlette.py | 60 +++++++++++++--------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 463385cee6..90f32a8c42 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -9,18 +9,12 @@ from sentry_sdk.consts import OP from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import ( - _is_json_content_type, - request_body_within_bounds, -) +from sentry_sdk.integrations._wsgi_common import (_is_json_content_type, + request_body_within_bounds) from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE -from sentry_sdk.utils import ( - AnnotatedValue, - capture_internal_exceptions, - event_from_exception, - transaction_from_function, -) +from sentry_sdk.utils import (AnnotatedValue, capture_internal_exceptions, + event_from_exception, transaction_from_function) if MYPY: from typing import Any, Awaitable, Callable, Dict, Optional @@ -32,18 +26,20 @@ from starlette.applications import Starlette # type: ignore from starlette.datastructures import UploadFile # type: ignore from starlette.middleware import Middleware # type: ignore - from starlette.middleware.authentication import ( # type: ignore - AuthenticationMiddleware, - ) + from starlette.middleware.authentication import \ + AuthenticationMiddleware # type: ignore from starlette.requests import Request # type: ignore from starlette.routing import Match # type: ignore - from starlette.types import ASGIApp, Receive, Scope as StarletteScope, Send # type: ignore + from starlette.types import ASGIApp, Receive + from starlette.types import Scope as StarletteScope # type: ignore + from starlette.types import Send except ImportError: raise DidNotEnable("Starlette is not installed") try: # Starlette 0.20 - from starlette.middleware.exceptions import ExceptionMiddleware # type: ignore + from starlette.middleware.exceptions import \ + ExceptionMiddleware # type: ignore except ImportError: # Startlette 0.19.1 from starlette.exceptions import ExceptionMiddleware # type: ignore @@ -210,23 +206,23 @@ async def _sentry_patched_exception_handler(self, *args, **kwargs): old_call = middleware_class.__call__ - async def _sentry_exceptionmiddleware_call(self, scope, receive, send): - # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None - # Also add the user (that was eventually set by be Authentication middle - # that was called before this middleware). This is done because the authentication - # middleware sets the user in the scope and then (in the same function) - # calls this exception middelware. In case there is no exception (or no handler - # for the type of exception occuring) then the exception bubbles up and setting the - # user information into the sentry scope is done in auth middleware and the - # ASGI middleware will then send everything to Sentry and this is fine. - # But if there is an exception happening that the exception middleware here - # has a handler for, it will send the exception directly to Sentry, so we need - # the user information right now. - # This is why we do it here. - _add_user_to_sentry_scope(scope) - await old_call(self, scope, receive, send) - - middleware_class.__call__ = _sentry_exceptionmiddleware_call + async def _sentry_exceptionmiddleware_call(self, scope, receive, send): + # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None + # Also add the user (that was eventually set by be Authentication middle + # that was called before this middleware). This is done because the authentication + # middleware sets the user in the scope and then (in the same function) + # calls this exception middelware. In case there is no exception (or no handler + # for the type of exception occuring) then the exception bubbles up and setting the + # user information into the sentry scope is done in auth middleware and the + # ASGI middleware will then send everything to Sentry and this is fine. + # But if there is an exception happening that the exception middleware here + # has a handler for, it will send the exception directly to Sentry, so we need + # the user information right now. + # This is why we do it here. + _add_user_to_sentry_scope(scope) + await old_call(self, scope, receive, send) + + middleware_class.__call__ = _sentry_exceptionmiddleware_call def _add_user_to_sentry_scope(scope): From 62e8a635ddf386faccf7378ea72f280745e597a5 Mon Sep 17 00:00:00 2001 From: Johnny Deuss Date: Thu, 19 Jan 2023 11:19:25 +0000 Subject: [PATCH 3/4] Blackify starlette.py --- sentry_sdk/integrations/starlette.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 90f32a8c42..f7b152a4f5 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -9,12 +9,18 @@ from sentry_sdk.consts import OP from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.integrations._wsgi_common import (_is_json_content_type, - request_body_within_bounds) +from sentry_sdk.integrations._wsgi_common import ( + _is_json_content_type, + request_body_within_bounds, +) from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE -from sentry_sdk.utils import (AnnotatedValue, capture_internal_exceptions, - event_from_exception, transaction_from_function) +from sentry_sdk.utils import ( + AnnotatedValue, + capture_internal_exceptions, + event_from_exception, + transaction_from_function, +) if MYPY: from typing import Any, Awaitable, Callable, Dict, Optional @@ -26,8 +32,9 @@ from starlette.applications import Starlette # type: ignore from starlette.datastructures import UploadFile # type: ignore from starlette.middleware import Middleware # type: ignore - from starlette.middleware.authentication import \ - AuthenticationMiddleware # type: ignore + from starlette.middleware.authentication import ( + AuthenticationMiddleware, + ) # type: ignore from starlette.requests import Request # type: ignore from starlette.routing import Match # type: ignore from starlette.types import ASGIApp, Receive @@ -38,8 +45,7 @@ try: # Starlette 0.20 - from starlette.middleware.exceptions import \ - ExceptionMiddleware # type: ignore + from starlette.middleware.exceptions import ExceptionMiddleware # type: ignore except ImportError: # Startlette 0.19.1 from starlette.exceptions import ExceptionMiddleware # type: ignore @@ -167,6 +173,7 @@ def patch_exception_middleware(middleware_class): not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init) if not_yet_patched: + def _sentry_middleware_init(self, *args, **kwargs): # type: (Any, Any, Any) -> None old_middleware_init(self, *args, **kwargs) From 89f71fb7bfdccdca57884c75f67a3d540b5a3e7b Mon Sep 17 00:00:00 2001 From: Johnny Deuss Date: Thu, 19 Jan 2023 12:38:22 +0000 Subject: [PATCH 4/4] Undo changes to imports --- sentry_sdk/integrations/starlette.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index f7b152a4f5..aec194a779 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -32,14 +32,12 @@ from starlette.applications import Starlette # type: ignore from starlette.datastructures import UploadFile # type: ignore from starlette.middleware import Middleware # type: ignore - from starlette.middleware.authentication import ( + from starlette.middleware.authentication import ( # type: ignore AuthenticationMiddleware, - ) # type: ignore + ) from starlette.requests import Request # type: ignore from starlette.routing import Match # type: ignore - from starlette.types import ASGIApp, Receive - from starlette.types import Scope as StarletteScope # type: ignore - from starlette.types import Send + from starlette.types import ASGIApp, Receive, Scope as StarletteScope, Send # type: ignore except ImportError: raise DidNotEnable("Starlette is not installed")