From 885aeafe47b84ff04db5575d751f385117120e24 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 22 Sep 2025 08:37:13 +0200 Subject: [PATCH 01/18] fix: read request body after handler so receive stream is not deprived --- sentry_sdk/integrations/fastapi.py | 9 ++- sentry_sdk/integrations/starlette.py | 50 +++++++++++++- tests/integrations/fastapi/test_fastapi.py | 26 ++++++++ .../integrations/starlette/test_starlette.py | 66 ++++++++++++++++++- 4 files changed, 146 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 1473cbcab7..85678ce185 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -18,6 +18,7 @@ from sentry_sdk.integrations.starlette import ( StarletteIntegration, StarletteRequestExtractor, + _patch_request, ) except DidNotEnable: raise DidNotEnable("Starlette is not installed") @@ -103,11 +104,16 @@ async def _sentry_app(*args, **kwargs): return await old_app(*args, **kwargs) request = args[0] + _patch_request(request) _set_transaction_name_and_source( sentry_sdk.get_current_scope(), integration.transaction_style, request ) sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope._name = FastApiIntegration.identifier + + response = await old_app(*args, **kwargs) + extractor = StarletteRequestExtractor(request) info = await extractor.extract_request_info() @@ -129,12 +135,11 @@ def event_processor(event, hint): return event_processor - sentry_scope._name = FastApiIntegration.identifier sentry_scope.add_event_processor( _make_request_event_processor(request, integration) ) - return await old_app(*args, **kwargs) + return response return _sentry_app diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index c7ce40618b..1efcdaa102 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -422,6 +422,36 @@ def _is_async_callable(obj): ) +def _patch_request(request): + _original_body = request.body + _original_json = request.json + _original_form = request.form + + def restore_original_methods(): + request.body = _original_body + request.json = _original_json + request.form = _original_form + + async def sentry_body(): + request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True + restore_original_methods() + return await _original_body() + + async def sentry_json(): + request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True + restore_original_methods() + return await _original_json() + + async def sentry_form(): + request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True + restore_original_methods() + return await _original_form() + + request.body = sentry_body + request.json = sentry_json + request.form = sentry_form + + def patch_request_response(): # type: () -> None old_request_response = starlette.routing.request_response @@ -442,6 +472,7 @@ async def _sentry_async_func(*args, **kwargs): return await old_func(*args, **kwargs) request = args[0] + _patch_request(request) _set_transaction_name_and_source( sentry_sdk.get_current_scope(), @@ -450,6 +481,10 @@ async def _sentry_async_func(*args, **kwargs): ) sentry_scope = sentry_sdk.get_isolation_scope() + sentry_scope._name = StarletteIntegration.identifier + + response = await old_func(*args, **kwargs) + extractor = StarletteRequestExtractor(request) info = await extractor.extract_request_info() @@ -471,12 +506,11 @@ def event_processor(event, hint): return event_processor - sentry_scope._name = StarletteIntegration.identifier sentry_scope.add_event_processor( _make_request_event_processor(request, integration) ) - return await old_func(*args, **kwargs) + return response func = _sentry_async_func @@ -623,6 +657,18 @@ async def extract_request_info(self): request_info["data"] = AnnotatedValue.removed_because_over_size_limit() return request_info + # Avoid hangs by not parsing body when ASGI stream is consumed + is_body_cached = ( + "state" in self.request.scope + and "sentry_sdk.is_body_cached" in self.request.scope["state"] + and self.request.scope["state"]["sentry_sdk.is_body_cached"] + ) + if self.request.is_disconnected() and not is_body_cached: + request_info["data"] = ( + AnnotatedValue.removed_because_body_consumed_and_not_cached() + ) + return request_info + # Add JSON body, if it is a JSON request json = await self.json() if json: diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 3d79da92cc..f09ca42632 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -24,6 +24,8 @@ from tests.integrations.conftest import parametrize_test_configurable_status_codes from tests.integrations.starlette import test_starlette +BODY_JSON = {"some": "json", "for": "testing", "nested": {"numbers": 123}} + def fastapi_app_factory(): app = FastAPI() @@ -72,6 +74,29 @@ async def _thread_ids_async(): return app +def test_stream_available_in_handler(sentry_init): + sentry_init( + integrations=[StarletteIntegration(), FastApiIntegration()], + ) + + app = FastAPI() + + @app.post("/consume") + async def _consume_stream_body(request): + # Avoid cache by constructing new request + wrapped_request = Request(request.scope, request.receive) + + assert await wrapped_request.json() == BODY_JSON + + return {"status": "ok"} + + client = TestClient(app) + client.post( + "/consume", + json=BODY_JSON, + ) + + @pytest.mark.asyncio async def test_response(sentry_init, capture_events): # FastAPI is heavily based on Starlette so we also need @@ -223,6 +248,7 @@ def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, en @pytest.mark.asyncio async def test_original_request_not_scrubbed(sentry_init, capture_events): sentry_init( + default_integrations=False, integrations=[StarletteIntegration(), FastApiIntegration()], traces_sample_rate=1.0, ) diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index bc445bf8f2..5df66165e7 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -26,6 +26,7 @@ AuthenticationError, SimpleUser, ) +from starlette.requests import Request from starlette.exceptions import HTTPException from starlette.middleware import Middleware from starlette.middleware.authentication import AuthenticationMiddleware @@ -435,6 +436,7 @@ async def test_starletterequestextractor_extract_request_info(sentry_init): side_effect = [_mock_receive(msg) for msg in JSON_RECEIVE_MESSAGES] starlette_request._receive = mock.Mock(side_effect=side_effect) + starlette_request.scope["state"] = {"sentry_sdk.is_body_cached": True} extractor = StarletteRequestExtractor(starlette_request) request_info = await extractor.extract_request_info() @@ -447,6 +449,37 @@ async def test_starletterequestextractor_extract_request_info(sentry_init): assert request_info["data"] == BODY_JSON +@pytest.mark.asyncio +async def test_starletterequestextractor_extract_request_info_not_cached(sentry_init): + sentry_init( + send_default_pii=True, + integrations=[StarletteIntegration()], + ) + scope = SCOPE.copy() + scope["headers"] = [ + [b"content-type", b"application/json"], + [b"content-length", str(len(json.dumps(BODY_JSON))).encode()], + [b"cookie", b"yummy_cookie=choco; tasty_cookie=strawberry"], + ] + + starlette_request = starlette.requests.Request(scope) + + # Mocking async `_receive()` that works in Python 3.7+ + side_effect = [_mock_receive(msg) for msg in JSON_RECEIVE_MESSAGES] + starlette_request._receive = mock.Mock(side_effect=side_effect) + + extractor = StarletteRequestExtractor(starlette_request) + + request_info = await extractor.extract_request_info() + + assert request_info + assert request_info["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } + assert request_info["data"].metadata == {"rem": [["!consumed", "x"]]} + + @pytest.mark.asyncio async def test_starletterequestextractor_extract_request_info_no_pii(sentry_init): sentry_init( @@ -466,6 +499,7 @@ async def test_starletterequestextractor_extract_request_info_no_pii(sentry_init side_effect = [_mock_receive(msg) for msg in JSON_RECEIVE_MESSAGES] starlette_request._receive = mock.Mock(side_effect=side_effect) + starlette_request.scope["state"] = {"sentry_sdk.is_body_cached": True} extractor = StarletteRequestExtractor(starlette_request) request_info = await extractor.extract_request_info() @@ -475,6 +509,32 @@ async def test_starletterequestextractor_extract_request_info_no_pii(sentry_init assert request_info["data"] == BODY_JSON +def test_stream_available_in_handler(sentry_init): + sentry_init( + integrations=[StarletteIntegration()], + ) + + async def _consume_stream_body(request): + # Avoid cache by constructing new request + wrapped_request = Request(request.scope, request.receive) + + assert await wrapped_request.json() == BODY_JSON + + return starlette.responses.JSONResponse({"status": "ok"}) + + app = starlette.applications.Starlette( + routes=[ + starlette.routing.Route("/consume", _consume_stream_body, methods=["POST"]), + ], + ) + + client = TestClient(app) + client.post( + "/consume", + json=BODY_JSON, + ) + + @pytest.mark.parametrize( "url,transaction_style,expected_transaction,expected_source", [ @@ -942,7 +1002,11 @@ def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, en def test_original_request_not_scrubbed(sentry_init, capture_events): - sentry_init(integrations=[StarletteIntegration()]) + sentry_init( + default_integrations=False, + integrations=[StarletteIntegration()], + traces_sample_rate=1.0, + ) events = capture_events() From 37b78cf91ad0138b167d07f51af4a723f06f70af Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 22 Sep 2025 09:05:40 +0200 Subject: [PATCH 02/18] Await coroutine and add new annotated value --- sentry_sdk/_types.py | 16 ++++++++++++++++ sentry_sdk/integrations/starlette.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index b28c7260ce..dc380d19f8 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -76,6 +76,22 @@ def removed_because_over_size_limit(cls, value=""): }, ) + @classmethod + def removed_because_body_consumed_and_not_cached(cls, value=""): + # type: (Any) -> AnnotatedValue + """The actual value was removed because the underlying stream has been consumed, without caching the value.""" + return AnnotatedValue( + value=value, + metadata={ + "rem": [ # Remark + [ + "!consumed", # Because the original stream is unavailable + "x", # The fields original value was removed + ] + ] + }, + ) + @classmethod def substituted_because_contains_sensitive_data(cls): # type: () -> AnnotatedValue diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 1efcdaa102..20f30b378a 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -663,7 +663,7 @@ async def extract_request_info(self): and "sentry_sdk.is_body_cached" in self.request.scope["state"] and self.request.scope["state"]["sentry_sdk.is_body_cached"] ) - if self.request.is_disconnected() and not is_body_cached: + if await self.request.is_disconnected() and not is_body_cached: request_info["data"] = ( AnnotatedValue.removed_because_body_consumed_and_not_cached() ) From ed0f7f7c50a77d9e4bd0a9a7a866e2bde812e2e9 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 22 Sep 2025 10:19:40 +0200 Subject: [PATCH 03/18] Add type annotations and attach graphene query again --- sentry_sdk/integrations/graphene.py | 27 ++++++++++++++++----------- sentry_sdk/integrations/starlette.py | 12 +++++++++--- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/integrations/graphene.py b/sentry_sdk/integrations/graphene.py index 00a8d155d4..786e666eee 100644 --- a/sentry_sdk/integrations/graphene.py +++ b/sentry_sdk/integrations/graphene.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: from collections.abc import Generator - from typing import Any, Dict, Union + from typing import Any, Dict, Union, Callable from graphene.language.source import Source # type: ignore from graphql.execution import ExecutionResult from graphql.type import GraphQLSchema @@ -48,7 +48,7 @@ def _patch_graphql(): def _sentry_patched_graphql_sync(schema, source, *args, **kwargs): # type: (GraphQLSchema, Union[str, Source], Any, Any) -> ExecutionResult scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) + scope.add_event_processor(_make_event_processor(source)) with graphql_span(schema, source, kwargs): result = old_graphql_sync(schema, source, *args, **kwargs) @@ -75,7 +75,7 @@ async def _sentry_patched_graphql_async(schema, source, *args, **kwargs): return await old_graphql_async(schema, source, *args, **kwargs) scope = sentry_sdk.get_isolation_scope() - scope.add_event_processor(_event_processor) + scope.add_event_processor(_make_event_processor(source)) with graphql_span(schema, source, kwargs): result = await old_graphql_async(schema, source, *args, **kwargs) @@ -99,16 +99,21 @@ async def _sentry_patched_graphql_async(schema, source, *args, **kwargs): graphene_schema.graphql = _sentry_patched_graphql_async -def _event_processor(event, hint): - # type: (Event, Dict[str, Any]) -> Event - if should_send_default_pii(): - request_info = event.setdefault("request", {}) - request_info["api_target"] = "graphql" +def _make_event_processor(source): + # type: (Any) -> Callable[[Event, dict[str, Any]], Event] + def _event_processor(event, hint): + # type: (Event, Dict[str, Any]) -> Event + if should_send_default_pii(): + request_info = event.setdefault("request", {}) + request_info["api_target"] = "graphql" + request_info["data"] = {"query": source} - elif event.get("request", {}).get("data"): - del event["request"]["data"] + elif event.get("request", {}).get("data"): + del event["request"]["data"] - return event + return event + + return _event_processor @contextmanager diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 20f30b378a..e48cb888c0 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -423,33 +423,38 @@ def _is_async_callable(obj): def _patch_request(request): + # type: (Request) -> None _original_body = request.body _original_json = request.json _original_form = request.form def restore_original_methods(): + # type: () -> None request.body = _original_body request.json = _original_json request.form = _original_form async def sentry_body(): + # type: () -> bytes request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True restore_original_methods() return await _original_body() async def sentry_json(): + # type: () -> Any request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True restore_original_methods() return await _original_json() async def sentry_form(): + # type: () -> Any request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True restore_original_methods() return await _original_form() - request.body = sentry_body - request.json = sentry_json - request.form = sentry_form + request.body = sentry_body # type: ignore + request.json = sentry_json # type: ignore + request.form = sentry_form # type: ignore def patch_request_response(): @@ -673,6 +678,7 @@ async def extract_request_info(self): json = await self.json() if json: request_info["data"] = json + print("in fastapi json", request_info) return request_info # Add form as key/value pairs, if request has form data From 670863aacde6442043c7d7f61c46e1c42c2a60ca Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 22 Sep 2025 10:27:47 +0200 Subject: [PATCH 04/18] mypy --- sentry_sdk/integrations/starlette.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index e48cb888c0..852cdc734a 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -452,9 +452,9 @@ async def sentry_form(): restore_original_methods() return await _original_form() - request.body = sentry_body # type: ignore - request.json = sentry_json # type: ignore - request.form = sentry_form # type: ignore + request.body = sentry_body + request.json = sentry_json + request.form = sentry_form def patch_request_response(): From 66d2ddea892d20295b93f9e55148054589ea1d7a Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 22 Sep 2025 10:58:37 +0200 Subject: [PATCH 05/18] . --- sentry_sdk/integrations/starlette.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 852cdc734a..b825488359 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -678,7 +678,6 @@ async def extract_request_info(self): json = await self.json() if json: request_info["data"] = json - print("in fastapi json", request_info) return request_info # Add form as key/value pairs, if request has form data From ddc65438fed430f3aee1aa65c0da2b9b56842da2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 22 Sep 2025 12:54:39 +0200 Subject: [PATCH 06/18] . --- sentry_sdk/integrations/starlette.py | 2 +- tests/integrations/fastapi/test_fastapi.py | 3 ++- tests/integrations/starlette/test_starlette.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index b825488359..0b63fb356f 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -668,7 +668,7 @@ async def extract_request_info(self): and "sentry_sdk.is_body_cached" in self.request.scope["state"] and self.request.scope["state"]["sentry_sdk.is_body_cached"] ) - if await self.request.is_disconnected() and not is_body_cached: + if not is_body_cached: request_info["data"] = ( AnnotatedValue.removed_because_body_consumed_and_not_cached() ) diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index f09ca42632..aa1e6b9c40 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -1,3 +1,4 @@ +import asyncio import json import logging import pytest @@ -86,7 +87,7 @@ async def _consume_stream_body(request): # Avoid cache by constructing new request wrapped_request = Request(request.scope, request.receive) - assert await wrapped_request.json() == BODY_JSON + assert await asyncio.wait_for(wrapped_request.json(), timeout=1.0) == BODY_JSON return {"status": "ok"} diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 5df66165e7..64270855e5 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -518,7 +518,7 @@ async def _consume_stream_body(request): # Avoid cache by constructing new request wrapped_request = Request(request.scope, request.receive) - assert await wrapped_request.json() == BODY_JSON + assert await asyncio.wait_for(wrapped_request.json(), timeout=1.0) == BODY_JSON return starlette.responses.JSONResponse({"status": "ok"}) From f66bcd633bba18efd39d4e0aa9116b42122e07aa Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 1 Oct 2025 10:06:03 +0200 Subject: [PATCH 07/18] wrap in try finally --- sentry_sdk/integrations/fastapi.py | 53 +++++++++++++++--------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 85678ce185..53f972ab77 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -112,32 +112,33 @@ async def _sentry_app(*args, **kwargs): sentry_scope = sentry_sdk.get_isolation_scope() sentry_scope._name = FastApiIntegration.identifier - response = await old_app(*args, **kwargs) - - extractor = StarletteRequestExtractor(request) - info = await extractor.extract_request_info() - - def _make_request_event_processor(req, integration): - # type: (Any, Any) -> Callable[[Event, Dict[str, Any]], Event] - def event_processor(event, hint): - # type: (Event, Dict[str, Any]) -> Event - - # Extract information from request - request_info = event.get("request", {}) - if info: - if "cookies" in info and should_send_default_pii(): - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) - - return event - - return event_processor - - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) + try: + response = await old_app(*args, **kwargs) + finally: + extractor = StarletteRequestExtractor(request) + info = await extractor.extract_request_info() + + def _make_request_event_processor(req, integration): + # type: (Any, Any) -> Callable[[Event, Dict[str, Any]], Event] + def event_processor(event, hint): + # type: (Event, Dict[str, Any]) -> Event + + # Extract information from request + request_info = event.get("request", {}) + if info: + if "cookies" in info and should_send_default_pii(): + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) return response From 28095bd82b22eaa43f50bca5d1f7c818d201dc4f Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 10:40:34 +0200 Subject: [PATCH 08/18] improve typing and simplify patching --- sentry_sdk/integrations/starlette.py | 57 ++++++++++++++++++---------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index a5039677d2..aa7e4a4d86 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -36,15 +36,26 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Awaitable, Callable, Container, Dict, Optional, Tuple, Union - + from typing import ( + Any, + Awaitable, + Callable, + Container, + Dict, + Optional, + Tuple, + Union, + Protocol, + TypeVar, + ) + from types import CoroutineType from sentry_sdk._types import Event, HttpStatusCodeRange try: import starlette # type: ignore from starlette import __version__ as STARLETTE_VERSION from starlette.applications import Starlette # type: ignore - from starlette.datastructures import UploadFile # type: ignore + from starlette.datastructures import UploadFile, FormData # type: ignore from starlette.middleware import Middleware # type: ignore from starlette.middleware.authentication import ( # type: ignore AuthenticationMiddleware, @@ -55,6 +66,16 @@ except ImportError: raise DidNotEnable("Starlette is not installed") +if TYPE_CHECKING: + from contextlib import AbstractAsyncContextManager + + T_co = TypeVar("T_co", covariant=True) + + class AwaitableOrContextManager( + Awaitable[T_co], AbstractAsyncContextManager[T_co], Protocol[T_co] + ): ... + + try: # Starlette 0.20 from starlette.middleware.exceptions import ExceptionMiddleware # type: ignore @@ -426,29 +447,23 @@ def _patch_request(request): _original_json = request.json _original_form = request.form - def restore_original_methods(): - # type: () -> None - request.body = _original_body - request.json = _original_json - request.form = _original_form - - async def sentry_body(): - # type: () -> bytes + @functools.wraps(_original_body) + def sentry_body(): + # type: () -> CoroutineType[Any, Any, bytes] request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True - restore_original_methods() - return await _original_body() + return _original_body() - async def sentry_json(): - # type: () -> Any + @functools.wraps(_original_json) + def sentry_json(): + # type: () -> CoroutineType[Any, Any, Any] request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True - restore_original_methods() - return await _original_json() + return _original_json() - async def sentry_form(): - # type: () -> Any + @functools.wraps(_original_form) + def sentry_form(*args, **kwargs): + # type: (*Any, **Any) -> AwaitableOrContextManager[FormData] request.scope.setdefault("state", {})["sentry_sdk.is_body_cached"] = True - restore_original_methods() - return await _original_form() + return _original_form(*args, **kwargs) request.body = sentry_body request.json = sentry_json From 11366c0fd00568b4c42ee943171e1b2eb72dbc94 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 14:13:55 +0200 Subject: [PATCH 09/18] add try except and more tests --- sentry_sdk/integrations/fastapi.py | 45 +++++---- sentry_sdk/integrations/starlette.py | 32 ++++--- tests/integrations/fastapi/test_fastapi.py | 86 +++++++++++++----- .../integrations/starlette/test_starlette.py | 91 +++++++++++++------ 4 files changed, 171 insertions(+), 83 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 53f972ab77..5e39bfd4d7 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -11,7 +11,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Callable, Dict + from typing import Any, Callable, Dict, Optional from sentry_sdk._types import Event try: @@ -112,33 +112,38 @@ async def _sentry_app(*args, **kwargs): sentry_scope = sentry_sdk.get_isolation_scope() sentry_scope._name = FastApiIntegration.identifier + def _make_request_event_processor(info): + # type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event] + def event_processor(event, hint): + # type: (Event, Dict[str, Any]) -> Event + + # Extract information from request + request_info = event.get("request", {}) + if info: + if "cookies" in info and should_send_default_pii(): + request_info["cookies"] = info["cookies"] + if "data" in info: + request_info["data"] = info["data"] + event["request"] = deepcopy(request_info) + + return event + + return event_processor + try: response = await old_app(*args, **kwargs) - finally: + except Exception as exception: extractor = StarletteRequestExtractor(request) info = await extractor.extract_request_info() - def _make_request_event_processor(req, integration): - # type: (Any, Any) -> Callable[[Event, Dict[str, Any]], Event] - def event_processor(event, hint): - # type: (Event, Dict[str, Any]) -> Event - - # Extract information from request - request_info = event.get("request", {}) - if info: - if "cookies" in info and should_send_default_pii(): - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) + sentry_scope.add_event_processor(_make_request_event_processor(info)) - return event + raise exception - return event_processor + extractor = StarletteRequestExtractor(request) + info = await extractor.extract_request_info() - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) + sentry_scope.add_event_processor(_make_request_event_processor(info)) return response diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index aa7e4a4d86..a556a567e7 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -501,20 +501,15 @@ async def _sentry_async_func(*args, **kwargs): sentry_scope = sentry_sdk.get_isolation_scope() sentry_scope._name = StarletteIntegration.identifier - response = await old_func(*args, **kwargs) - - extractor = StarletteRequestExtractor(request) - info = await extractor.extract_request_info() - - def _make_request_event_processor(req, integration): - # type: (Any, Any) -> Callable[[Event, dict[str, Any]], Event] + def _make_request_event_processor(info): + # type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event] def event_processor(event, hint): # type: (Event, Dict[str, Any]) -> Event - # Add info from request to event + # Extract information from request request_info = event.get("request", {}) if info: - if "cookies" in info: + if "cookies" in info and should_send_default_pii(): request_info["cookies"] = info["cookies"] if "data" in info: request_info["data"] = info["data"] @@ -524,9 +519,22 @@ def event_processor(event, hint): return event_processor - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) + try: + response = await old_func(*args, **kwargs) + except Exception as exception: + extractor = StarletteRequestExtractor(request) + info = await extractor.extract_request_info() + + sentry_scope.add_event_processor( + _make_request_event_processor(info) + ) + + raise exception + + extractor = StarletteRequestExtractor(request) + info = await extractor.extract_request_info() + + sentry_scope.add_event_processor(_make_request_event_processor(info)) return response diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 3ff8d51d8b..2160d46eaf 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -12,7 +12,7 @@ from fastapi.middleware.trustedhost import TrustedHostMiddleware import sentry_sdk -from sentry_sdk import capture_message +from sentry_sdk import capture_message, capture_exception from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.integrations.fastapi import FastApiIntegration @@ -75,29 +75,6 @@ async def _thread_ids_async(): return app -def test_stream_available_in_handler(sentry_init): - sentry_init( - integrations=[StarletteIntegration(), FastApiIntegration()], - ) - - app = FastAPI() - - @app.post("/consume") - async def _consume_stream_body(request): - # Avoid cache by constructing new request - wrapped_request = Request(request.scope, request.receive) - - assert await asyncio.wait_for(wrapped_request.json(), timeout=1.0) == BODY_JSON - - return {"status": "ok"} - - client = TestClient(app) - client.post( - "/consume", - json=BODY_JSON, - ) - - @pytest.mark.asyncio async def test_response(sentry_init, capture_events): # FastAPI is heavily based on Starlette so we also need @@ -246,6 +223,67 @@ def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, en assert str(data["active"]) == trace_context["data"]["thread.id"] +@pytest.mark.asyncio +def test_request_body_not_cached_with_exception(sentry_init, capture_events): + sentry_init( + integrations=[StarletteIntegration(), FastApiIntegration()], + ) + + app = FastAPI() + + @app.post("/exception") + async def _exception(request: Request): + 1 / 0 + return {"error": "Oh no!"} + + events = capture_events() + + client = TestClient(app) + + try: + client.post( + "/exception", + json=BODY_JSON, + ) + except Exception: + capture_exception() + + event = events[0] + assert event["request"]["data"] == "" + + +def test_request_body_cached_with_exception(sentry_init, capture_events): + sentry_init( + integrations=[StarletteIntegration(), FastApiIntegration()], + ) + + app = FastAPI(debug=True) + app.user_middleware = [] + app.middleware_stack = app.build_middleware_stack() + + @app.post("/exception") + async def _exception(request: Request): + await request.json() + 1 / 0 + return {"error": "Oh no!"} + + events = capture_events() + + client = TestClient(app) + + try: + client.post( + "/exception", + json=BODY_JSON, + ) + except Exception: + print("capturing") + capture_exception() + + event = events[0] + assert event["request"]["data"] == BODY_JSON + + @pytest.mark.asyncio async def test_original_request_not_scrubbed(sentry_init, capture_events): sentry_init( diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 64270855e5..02ceb7b63a 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -11,7 +11,7 @@ import pytest -from sentry_sdk import capture_message, get_baggage, get_traceparent +from sentry_sdk import capture_message, get_baggage, get_traceparent, capture_exception from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.integrations.starlette import ( StarletteIntegration, @@ -509,32 +509,6 @@ async def test_starletterequestextractor_extract_request_info_no_pii(sentry_init assert request_info["data"] == BODY_JSON -def test_stream_available_in_handler(sentry_init): - sentry_init( - integrations=[StarletteIntegration()], - ) - - async def _consume_stream_body(request): - # Avoid cache by constructing new request - wrapped_request = Request(request.scope, request.receive) - - assert await asyncio.wait_for(wrapped_request.json(), timeout=1.0) == BODY_JSON - - return starlette.responses.JSONResponse({"status": "ok"}) - - app = starlette.applications.Starlette( - routes=[ - starlette.routing.Route("/consume", _consume_stream_body, methods=["POST"]), - ], - ) - - client = TestClient(app) - client.post( - "/consume", - json=BODY_JSON, - ) - - @pytest.mark.parametrize( "url,transaction_style,expected_transaction,expected_source", [ @@ -1001,6 +975,69 @@ def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, en assert str(data["active"]) == trace_context["data"]["thread.id"] +def test_request_body_not_cached_with_exception(sentry_init, capture_events): + sentry_init( + integrations=[StarletteIntegration()], + ) + + events = capture_events() + + async def _exception(request): + 1 / 0 + return {"error": "Oh no!"} + + app = starlette.applications.Starlette( + routes=[ + starlette.routing.Route("/exception", _exception, methods=["POST"]), + ], + ) + + client = TestClient(app) + + try: + client.post( + "/exception", + json=BODY_JSON, + ) + except ZeroDivisionError: + capture_exception() + + event = events[0] + assert event["request"]["data"] == "" + + +def test_request_body_cached_with_exception(sentry_init, capture_events): + sentry_init( + integrations=[StarletteIntegration()], + ) + + events = capture_events() + + async def _exception(request): + request.json() + 1 / 0 + return {"error": "Oh no!"} + + app = starlette.applications.Starlette( + routes=[ + starlette.routing.Route("/exception", _exception, methods=["POST"]), + ], + ) + + client = TestClient(app) + + try: + client.post( + "/exception", + json=BODY_JSON, + ) + except ZeroDivisionError: + capture_exception() + + event = events[0] + assert event["request"]["data"] == BODY_JSON + + def test_original_request_not_scrubbed(sentry_init, capture_events): sentry_init( default_integrations=False, From ea3c43c6986a328f40c9f8990f0be2ace1e9d4bd Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 14:41:25 +0200 Subject: [PATCH 10/18] cleanup --- sentry_sdk/integrations/starlette.py | 2 +- tests/integrations/fastapi/test_fastapi.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index a556a567e7..9bc3d208e7 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -573,7 +573,7 @@ def _make_request_event_processor(req, integration): def event_processor(event, hint): # type: (Event, dict[str, Any]) -> Event - # Extract information from request + # Add info from request to event request_info = event.get("request", {}) if cookies: request_info["cookies"] = cookies diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 2160d46eaf..91944585e2 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -223,7 +223,6 @@ def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, en assert str(data["active"]) == trace_context["data"]["thread.id"] -@pytest.mark.asyncio def test_request_body_not_cached_with_exception(sentry_init, capture_events): sentry_init( integrations=[StarletteIntegration(), FastApiIntegration()], @@ -257,9 +256,7 @@ def test_request_body_cached_with_exception(sentry_init, capture_events): integrations=[StarletteIntegration(), FastApiIntegration()], ) - app = FastAPI(debug=True) - app.user_middleware = [] - app.middleware_stack = app.build_middleware_stack() + app = FastAPI() @app.post("/exception") async def _exception(request: Request): From 27ae466c0491df66585835affcbd22e794e312de Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 14:46:16 +0200 Subject: [PATCH 11/18] cleanup --- tests/integrations/fastapi/test_fastapi.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 91944585e2..b612145863 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -244,7 +244,7 @@ async def _exception(request: Request): "/exception", json=BODY_JSON, ) - except Exception: + except ZeroDivisionError: capture_exception() event = events[0] @@ -273,8 +273,7 @@ async def _exception(request: Request): "/exception", json=BODY_JSON, ) - except Exception: - print("capturing") + except ZeroDivisionError: capture_exception() event = events[0] From 32ea46fc65f7854a3b518dd080e659514c2495cf Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 14:47:56 +0200 Subject: [PATCH 12/18] cleanup --- sentry_sdk/integrations/starlette.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 9bc3d208e7..b16d4b7ce8 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -506,7 +506,7 @@ def _make_request_event_processor(info): def event_processor(event, hint): # type: (Event, Dict[str, Any]) -> Event - # Extract information from request + # Add info from request to event request_info = event.get("request", {}) if info: if "cookies" in info and should_send_default_pii(): @@ -573,7 +573,7 @@ def _make_request_event_processor(req, integration): def event_processor(event, hint): # type: (Event, dict[str, Any]) -> Event - # Add info from request to event + # Extract information from request request_info = event.get("request", {}) if cookies: request_info["cookies"] = cookies From c4e1232852930012046afbd8d5a89f83ed9a33ef Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 15:42:40 +0200 Subject: [PATCH 13/18] always attach cookies --- sentry_sdk/integrations/fastapi.py | 36 ++++++----- sentry_sdk/integrations/starlette.py | 36 ++++++----- tests/integrations/fastapi/test_fastapi.py | 54 ++++++++++++++++ .../integrations/starlette/test_starlette.py | 62 +++++++++++++++++-- 4 files changed, 154 insertions(+), 34 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 5e39bfd4d7..c269fbae0e 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -112,38 +112,44 @@ async def _sentry_app(*args, **kwargs): sentry_scope = sentry_sdk.get_isolation_scope() sentry_scope._name = FastApiIntegration.identifier - def _make_request_event_processor(info): + def _make_cookies_event_processor(cookies): # type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event] def event_processor(event, hint): # type: (Event, Dict[str, Any]) -> Event + if cookies and should_send_default_pii(): + event.get("request", {})["cookies"] = deepcopy(cookies) - # Extract information from request - request_info = event.get("request", {}) - if info: - if "cookies" in info and should_send_default_pii(): - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) + return event + + return event_processor + + def _make_request_body_event_processor(info): + # type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event] + def event_processor(event, hint): + # type: (Event, Dict[str, Any]) -> Event + if info and "data" in info: + event.get("request", {})["data"] = deepcopy(info["data"]) return event return event_processor + extractor = StarletteRequestExtractor(request) + cookies = extractor.extract_cookies_from_request() + sentry_scope.add_event_processor(_make_cookies_event_processor(cookies)) + try: response = await old_app(*args, **kwargs) except Exception as exception: - extractor = StarletteRequestExtractor(request) info = await extractor.extract_request_info() - - sentry_scope.add_event_processor(_make_request_event_processor(info)) + sentry_scope.add_event_processor( + _make_request_body_event_processor(info) + ) raise exception - extractor = StarletteRequestExtractor(request) info = await extractor.extract_request_info() - - sentry_scope.add_event_processor(_make_request_event_processor(info)) + sentry_scope.add_event_processor(_make_request_body_event_processor(info)) return response diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index b16d4b7ce8..c2449872ac 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -501,40 +501,46 @@ async def _sentry_async_func(*args, **kwargs): sentry_scope = sentry_sdk.get_isolation_scope() sentry_scope._name = StarletteIntegration.identifier - def _make_request_event_processor(info): + def _make_cookies_event_processor(cookies): # type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event] def event_processor(event, hint): # type: (Event, Dict[str, Any]) -> Event + if cookies and should_send_default_pii(): + event.get("request", {})["cookies"] = deepcopy(cookies) - # Add info from request to event - request_info = event.get("request", {}) - if info: - if "cookies" in info and should_send_default_pii(): - request_info["cookies"] = info["cookies"] - if "data" in info: - request_info["data"] = info["data"] - event["request"] = deepcopy(request_info) + return event + + return event_processor + + def _make_request_body_event_processor(info): + # type: (Optional[Dict[str, Any]]) -> Callable[[Event, Dict[str, Any]], Event] + def event_processor(event, hint): + # type: (Event, Dict[str, Any]) -> Event + if info and "data" in info: + event.get("request", {})["data"] = deepcopy(info["data"]) return event return event_processor + extractor = StarletteRequestExtractor(request) + cookies = extractor.extract_cookies_from_request() + sentry_scope.add_event_processor(_make_cookies_event_processor(cookies)) + try: response = await old_func(*args, **kwargs) except Exception as exception: - extractor = StarletteRequestExtractor(request) info = await extractor.extract_request_info() - sentry_scope.add_event_processor( - _make_request_event_processor(info) + _make_request_body_event_processor(info) ) raise exception - extractor = StarletteRequestExtractor(request) info = await extractor.extract_request_info() - - sentry_scope.add_event_processor(_make_request_event_processor(info)) + sentry_scope.add_event_processor( + _make_request_body_event_processor(info) + ) return response diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index b612145863..30d10440b7 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -223,9 +223,46 @@ def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, en assert str(data["active"]) == trace_context["data"]["thread.id"] +def test_cookies_available(sentry_init, capture_events): + sentry_init( + integrations=[StarletteIntegration(), FastApiIntegration()], + send_default_pii=True, + ) + + app = FastAPI() + + @app.post("/exception") + async def _exception(request: Request): + logging.critical("Oh no!") + return {"error": "Oh no!"} + + events = capture_events() + + client = TestClient(app) + + try: + client.post( + "/exception", + json=BODY_JSON, + cookies={ + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + }, + ) + except ZeroDivisionError: + capture_exception() + + event = events[0] + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } + + def test_request_body_not_cached_with_exception(sentry_init, capture_events): sentry_init( integrations=[StarletteIntegration(), FastApiIntegration()], + send_default_pii=True, ) app = FastAPI() @@ -243,17 +280,26 @@ async def _exception(request: Request): client.post( "/exception", json=BODY_JSON, + cookies={ + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + }, ) except ZeroDivisionError: capture_exception() event = events[0] + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } assert event["request"]["data"] == "" def test_request_body_cached_with_exception(sentry_init, capture_events): sentry_init( integrations=[StarletteIntegration(), FastApiIntegration()], + send_default_pii=True, ) app = FastAPI() @@ -272,11 +318,19 @@ async def _exception(request: Request): client.post( "/exception", json=BODY_JSON, + cookies={ + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + }, ) except ZeroDivisionError: capture_exception() event = events[0] + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } assert event["request"]["data"] == BODY_JSON diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 02ceb7b63a..6489b9ac94 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -975,16 +975,53 @@ def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, en assert str(data["active"]) == trace_context["data"]["thread.id"] -def test_request_body_not_cached_with_exception(sentry_init, capture_events): +def test_cookies_available(sentry_init, capture_events): sentry_init( integrations=[StarletteIntegration()], + send_default_pii=True, + ) + + events = capture_events() + + async def _exception(request: Request): + logging.critical("Oh no!") + return starlette.responses.JSONResponse({"status": "Oh no!"}) + + app = starlette.applications.Starlette( + routes=[ + starlette.routing.Route("/exception", _exception, methods=["POST"]), + ], + ) + + client = TestClient(app) + + client.post( + "/exception", + json=BODY_JSON, + cookies={ + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + }, + ) + + event = events[0] + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } + + +def test_request_body_not_cached_exception(sentry_init, capture_events): + sentry_init( + integrations=[StarletteIntegration()], + send_default_pii=True, ) events = capture_events() async def _exception(request): 1 / 0 - return {"error": "Oh no!"} + return starlette.responses.JSONResponse({"status": "Oh no!"}) app = starlette.applications.Starlette( routes=[ @@ -998,17 +1035,26 @@ async def _exception(request): client.post( "/exception", json=BODY_JSON, + cookies={ + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + }, ) except ZeroDivisionError: capture_exception() event = events[0] + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } assert event["request"]["data"] == "" -def test_request_body_cached_with_exception(sentry_init, capture_events): +def test_request_body_cached_exception(sentry_init, capture_events): sentry_init( integrations=[StarletteIntegration()], + send_default_pii=True, ) events = capture_events() @@ -1016,7 +1062,7 @@ def test_request_body_cached_with_exception(sentry_init, capture_events): async def _exception(request): request.json() 1 / 0 - return {"error": "Oh no!"} + return starlette.responses.JSONResponse({"status": "Oh no!"}) app = starlette.applications.Starlette( routes=[ @@ -1030,11 +1076,19 @@ async def _exception(request): client.post( "/exception", json=BODY_JSON, + cookies={ + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + }, ) except ZeroDivisionError: capture_exception() event = events[0] + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } assert event["request"]["data"] == BODY_JSON From 9d0e1670e4b95a72bf32a4a6b8552b2ae1928401 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 16:24:35 +0200 Subject: [PATCH 14/18] add missing await --- tests/integrations/starlette/test_starlette.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 6489b9ac94..554e78e980 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -1060,7 +1060,7 @@ def test_request_body_cached_exception(sentry_init, capture_events): events = capture_events() async def _exception(request): - request.json() + await request.json() 1 / 0 return starlette.responses.JSONResponse({"status": "Oh no!"}) From 8fe10b52b26a73c65f691e2155fa5ad80d2a3791 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 16:29:13 +0200 Subject: [PATCH 15/18] use setdefault --- sentry_sdk/integrations/graphene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/graphene.py b/sentry_sdk/integrations/graphene.py index 786e666eee..374b95584c 100644 --- a/sentry_sdk/integrations/graphene.py +++ b/sentry_sdk/integrations/graphene.py @@ -106,7 +106,7 @@ def _event_processor(event, hint): if should_send_default_pii(): request_info = event.setdefault("request", {}) request_info["api_target"] = "graphql" - request_info["data"] = {"query": source} + request_info.setdefault("data", {})["query"] = source elif event.get("request", {}).get("data"): del event["request"]["data"] From 6daea1933068bcbc012a2ea6cf192cd1c8c45d58 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 16:37:41 +0200 Subject: [PATCH 16/18] add type: ignore --- sentry_sdk/integrations/graphene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/graphene.py b/sentry_sdk/integrations/graphene.py index 374b95584c..1c85d597f2 100644 --- a/sentry_sdk/integrations/graphene.py +++ b/sentry_sdk/integrations/graphene.py @@ -106,7 +106,7 @@ def _event_processor(event, hint): if should_send_default_pii(): request_info = event.setdefault("request", {}) request_info["api_target"] = "graphql" - request_info.setdefault("data", {})["query"] = source + request_info.setdefault("data", {})["query"] = source # type: ignore elif event.get("request", {}).get("data"): del event["request"]["data"] From 7b7b9d10bf89eed719cdc7cfaab95b95cc95a7e5 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 16:46:02 +0200 Subject: [PATCH 17/18] use setdefault instead of get --- sentry_sdk/integrations/fastapi.py | 4 ++-- sentry_sdk/integrations/starlette.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index c269fbae0e..2235199d5c 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -117,7 +117,7 @@ def _make_cookies_event_processor(cookies): def event_processor(event, hint): # type: (Event, Dict[str, Any]) -> Event if cookies and should_send_default_pii(): - event.get("request", {})["cookies"] = deepcopy(cookies) + event.setdefault("request", {})["cookies"] = deepcopy(cookies) return event @@ -128,7 +128,7 @@ def _make_request_body_event_processor(info): def event_processor(event, hint): # type: (Event, Dict[str, Any]) -> Event if info and "data" in info: - event.get("request", {})["data"] = deepcopy(info["data"]) + event.setdefault("request", {})["data"] = deepcopy(info["data"]) return event diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index c2449872ac..029b5b8a09 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -506,7 +506,9 @@ def _make_cookies_event_processor(cookies): def event_processor(event, hint): # type: (Event, Dict[str, Any]) -> Event if cookies and should_send_default_pii(): - event.get("request", {})["cookies"] = deepcopy(cookies) + event.setdefault("request", {})["cookies"] = deepcopy( + cookies + ) return event @@ -517,7 +519,9 @@ def _make_request_body_event_processor(info): def event_processor(event, hint): # type: (Event, Dict[str, Any]) -> Event if info and "data" in info: - event.get("request", {})["data"] = deepcopy(info["data"]) + event.setdefault("request", {})["data"] = deepcopy( + info["data"] + ) return event From 6c9bba38a5d38d4be5028198a417c78f15cff363 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 6 Oct 2025 16:48:08 +0200 Subject: [PATCH 18/18] make adding query more robust --- sentry_sdk/integrations/graphene.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/graphene.py b/sentry_sdk/integrations/graphene.py index 1c85d597f2..00c474a276 100644 --- a/sentry_sdk/integrations/graphene.py +++ b/sentry_sdk/integrations/graphene.py @@ -106,7 +106,8 @@ def _event_processor(event, hint): if should_send_default_pii(): request_info = event.setdefault("request", {}) request_info["api_target"] = "graphql" - request_info.setdefault("data", {})["query"] = source # type: ignore + if isinstance(source, str): + request_info.setdefault("data", {})["query"] = source # type: ignore elif event.get("request", {}).get("data"): del event["request"]["data"]