diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 66274f2963..2c2bcd77d9 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -21,6 +21,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ) ``` +- The FastAPI and Starlette integrations no longer eagerly consume the request body. As a result, the body is only reported in events if your handler parsed it with `Request.json()` or `Request.form()` before the event is captured. - The UnraisableHookIntegration is now enabled by default. - We now don't suppress chained exceptions in the ASGI and asyncio integrations by default. The related `suppress_asgi_chained_exceptions` experimental option was removed. - In the AWS Lambda and GCP integrations, the message of the warning the SDK optionally emits if a function is about to time out has changed. diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 5f922a23d8..e428e398cd 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -126,7 +126,6 @@ async def _wrap_async_handler( ) sentry_scope = sentry_sdk.get_isolation_scope() extractor = StarletteRequestExtractor(request) - info = await extractor.extract_request_info() def _make_request_event_processor( req: "Any", integration: "Any" @@ -134,6 +133,8 @@ def _make_request_event_processor( def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": # Extract information from request request_info = event.get("request", {}) + + info = extractor.extract_request_info() if info: if "cookies" in info: request_info["cookies"] = info["cookies"] diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 63f30461fd..7463751fa3 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -531,14 +531,14 @@ async def _wrap_async_handler( sentry_scope = sentry_sdk.get_isolation_scope() extractor = StarletteRequestExtractor(request) - info = await extractor.extract_request_info() - def _make_request_event_processor( req: "Any", integration: "Any" ) -> "Callable[[Event, dict[str, Any]], Event]": def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": # Add info from request to event request_info = event.get("request", {}) + + info = extractor.extract_request_info() if info: if "cookies" in info: request_info["cookies"] = info["cookies"] @@ -743,7 +743,7 @@ def extract_cookies_from_request( return cookies - async def extract_request_info( + def extract_request_info( self: "StarletteRequestExtractor", ) -> "Optional[Dict[str, Any]]": client = sentry_sdk.get_client() @@ -763,7 +763,7 @@ async def extract_request_info( request_info["cookies"] = self.cookies() # If there is no body, just return the cookies - content_length = await self.content_length() + content_length = self.content_length() if not content_length: return request_info @@ -774,32 +774,30 @@ async def extract_request_info( request_info["data"] = AnnotatedValue.removed_because_over_size_limit() return request_info - # Add JSON body, if it is a JSON request - json = await self.json() - if json: - request_info["data"] = json + if hasattr(self.request, "_json"): + request_info["data"] = self.request._json return request_info - # Add form as key/value pairs, if request has form data - form = await self.form() - if form: - form_data = {} - for key, val in form.items(): - is_file = isinstance(val, UploadFile) - form_data[key] = ( - val - if not is_file - else AnnotatedValue.removed_because_raw_data() - ) + formdata_body = getattr(self.request, "_form", None) + if formdata_body is None and hasattr(self.request, "_body"): + # Raw data, do not add body just an annotation + request_info["data"] = AnnotatedValue.removed_because_raw_data() + return request_info - request_info["data"] = form_data + if formdata_body is None: return request_info - # Raw data, do not add body just an annotation - request_info["data"] = AnnotatedValue.removed_because_raw_data() + form_data = {} + for key, val in formdata_body.items(): + is_file = isinstance(val, UploadFile) + form_data[key] = ( + val if not is_file else AnnotatedValue.removed_because_raw_data() + ) + + request_info["data"] = form_data return request_info - async def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": + def content_length(self: "StarletteRequestExtractor") -> "Optional[int]": if "content-length" in self.request.headers: return int(self.request.headers["content-length"]) diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 7668f543b0..3f261d523b 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -495,6 +495,7 @@ async def test_original_request_not_scrubbed(sentry_init, capture_events): @app.post("/error") async def _error(request: Request): + await request.json() logging.critical("Oh no!") assert request.headers["Authorization"] == "Bearer ohno" assert request.headers["Proxy-Authorization"] == "Basic ohno" diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 41b905746f..3c657a6439 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -1500,6 +1500,7 @@ def test_original_request_not_scrubbed(sentry_init, capture_events): events = capture_events() async def _error(request): + await request.json() logging.critical("Oh no!") assert request.headers["Authorization"] == "Bearer ohno" assert request.headers["Proxy-Authorization"] == "Basic ohno" diff --git a/tests/integrations/strawberry/test_strawberry.py b/tests/integrations/strawberry/test_strawberry.py index 7900638155..7708114ae0 100644 --- a/tests/integrations/strawberry/test_strawberry.py +++ b/tests/integrations/strawberry/test_strawberry.py @@ -24,6 +24,7 @@ SentrySyncExtension, StrawberryIntegration, ) +from tests.conftest import ApproxDict try: from strawberry.extensions.tracing import ( @@ -339,11 +340,12 @@ def test_event_processor_data_collection( # request.data comes from the framework integration and must not be # overwritten by the strawberry integration - assert error_event["request"]["data"] == { - "query": query, - "operationName": "ErrorQuery", - "variables": {"value": "boom"}, - } + assert error_event["request"]["data"] == ApproxDict( + { + "operationName": "ErrorQuery", + "variables": {"value": "boom"}, + } + ) if expect_api_target: assert error_event["request"]["api_target"] == "graphql"