From cc30819995a4488abbc9bfbe3a5a859a44e78dfe Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 18 May 2026 16:53:40 +0200 Subject: [PATCH 1/4] fix(fastapi): Stop eagerly consuming request bodies for streamed spans --- sentry_sdk/integrations/fastapi.py | 108 +++--- sentry_sdk/integrations/starlette.py | 12 - tests/integrations/fastapi/test_fastapi.py | 414 ++++++++++++++------- 3 files changed, 342 insertions(+), 192 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 5833e5f290..e0061a2340 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -4,15 +4,16 @@ from typing import TYPE_CHECKING import sentry_sdk +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable from sentry_sdk.scope import should_send_default_pii -from sentry_sdk.traces import StreamedSpan +from sentry_sdk.traces import StreamedSpan, _get_current_streamed_span from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import transaction_from_function if TYPE_CHECKING: - from typing import Any, Callable, Dict + from typing import Any, Awaitable, Callable, Dict from sentry_sdk._types import Event @@ -20,7 +21,7 @@ from sentry_sdk.integrations.starlette import ( StarletteIntegration, StarletteRequestExtractor, - _set_request_body_data_on_streaming_segment, + _get_cached_request_body_attribute, ) except DidNotEnable: raise DidNotEnable("Starlette is not installed") @@ -75,6 +76,66 @@ def _set_transaction_name_and_source( scope.set_transaction_name(name, source=source) +async def _wrap_async_handler( + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" +) -> "Any": + """ + Wraps an asynchronous handler function to attach request info to errors and the server segment span. + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event + processor can still cause application hangs. + """ + client = sentry_sdk.get_client() + integration = client.get_integration(FastApiIntegration) + if integration is None: + return await handler(*args, **kwargs) + + request = args[0] + + _set_transaction_name_and_source( + sentry_sdk.get_current_scope(), integration.transaction_style, request + ) + 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": + # 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._name = FastApiIntegration.identifier + sentry_scope.add_event_processor( + _make_request_event_processor(request, integration) + ) + + try: + return await handler(*args, **kwargs) + finally: + current_span = _get_current_streamed_span() + + if type(current_span) is StreamedSpan: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + + def patch_get_request_handler() -> None: old_get_request_handler = fastapi.routing.get_request_handler @@ -113,46 +174,7 @@ def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any": old_app = old_get_request_handler(*args, **kwargs) async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - integration = client.get_integration(FastApiIntegration) - if integration is None: - return await old_app(*args, **kwargs) - - request = args[0] - - _set_transaction_name_and_source( - sentry_sdk.get_current_scope(), integration.transaction_style, request - ) - 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": - # 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._name = FastApiIntegration.identifier - sentry_scope.add_event_processor( - _make_request_event_processor(request, integration) - ) - - if has_span_streaming_enabled(client.options): - _set_request_body_data_on_streaming_segment(info) - - return await old_app(*args, **kwargs) + return await _wrap_async_handler(old_app, *args, **kwargs) return _sentry_app diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 38dbab8a26..5f511b3716 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -254,18 +254,6 @@ def _default(value: "Any") -> "Any": return json.dumps(data, default=_default) -def _set_request_body_data_on_streaming_segment( - info: "Optional[Dict[str, Any]]", -) -> None: - current_span = _get_current_streamed_span() - if info and "data" in info and type(current_span) is StreamedSpan: - with capture_internal_exceptions(): - current_span._segment.set_attribute( - "http.request.body.data", - _serialize_request_body_data(info["data"]), - ) - - @ensure_integration_enabled(StarletteIntegration) def _capture_exception(exception: BaseException, handled: "Any" = False) -> None: event, hint = event_from_exception( diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 25f27e2f49..7a85a06392 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -1,18 +1,23 @@ +import base64 import json import logging +import os import threading import warnings +from typing import Annotated from unittest import mock import fastapi import pytest import starlette -from fastapi import FastAPI, HTTPException, Request +from fastapi import Body, FastAPI, File, Form, HTTPException, Request, UploadFile from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.testclient import TestClient +from starlette.responses import JSONResponse import sentry_sdk from sentry_sdk import capture_message +from sentry_sdk.consts import SPANDATA from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.integrations.fastapi import FastApiIntegration @@ -22,6 +27,28 @@ FASTAPI_VERSION = parse_version(fastapi.__version__) STARLETTE_VERSION = parse_version(starlette.__version__) +PICTURE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "photo.jpg") + +BODY_JSON = {"some": "json", "for": "testing", "nested": {"numbers": 123}} + +BODY_FORM = """--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="username"\r\n\r\nJane\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="password"\r\n\r\nhello123\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="photo"; filename="photo.jpg"\r\nContent-Type: image/jpg\r\nContent-Transfer-Encoding: base64\r\n\r\n{{image_data}}\r\n--fd721ef49ea403a6--\r\n""".replace( + "{{image_data}}", str(base64.b64encode(open(PICTURE, "rb").read())) +) + +PARSED_FORM = starlette.datastructures.FormData( + [ + ("username", "Jane"), + ("password", "hello123"), + ( + "photo", + starlette.datastructures.UploadFile( + filename="photo.jpg", + file=open(PICTURE, "rb"), + ), + ), + ] +) + from tests.integrations.conftest import parametrize_test_configurable_status_codes from tests.integrations.starlette import test_starlette @@ -70,9 +97,258 @@ async def _thread_ids_async(): "active": str(threading.current_thread().ident), } + @app.post("/body/json") + async def body_json(payload: dict = Body(...)): + capture_message("hi") + return {"status": "ok"} + + @app.post("/body/form") + async def body_form( + username: Annotated[str, Form()], + password: Annotated[str, Form()], + photo: Annotated[UploadFile, File()], + ): + capture_message("hi") + return {"status": "ok"} + + @app.post("/body/raw") + async def body_raw(request: Request): + await request.body() + capture_message("hi") + return JSONResponse({"status": "ok"}) + return app +@pytest.mark.asyncio +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_request_info_json_body( + sentry_init, capture_events, capture_items, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + send_default_pii=True, + integrations=[StarletteIntegration()], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) + + app = fastapi_app_factory() + client = TestClient(app) + + if span_streaming: + items = capture_items("event", "span") + + client.post( + "/body/json", + json=BODY_JSON, + headers={ + "cookie": "yummy_cookie=choco; tasty_cookie=strawberry", + }, + ) + + (event,) = (item.payload for item in items if item.type == "event") + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } + assert event["request"]["data"] == BODY_JSON + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + server_span = next( + span for span in spans if span["attributes"]["sentry.op"] == "http.server" + ) + + assert json.loads( + server_span["attributes"][SPANDATA.HTTP_REQUEST_BODY_DATA] + ) == {"some": "json", "for": "testing", "nested": {"numbers": 123}} + else: + events = capture_events() + + client.post( + "/body/json", + json=BODY_JSON, + headers={ + "cookie": "yummy_cookie=choco; tasty_cookie=strawberry", + }, + ) + + (event, transaction_event) = events + + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } + assert event["request"]["data"] == BODY_JSON + + assert transaction_event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } + assert transaction_event["request"]["data"] == BODY_JSON + + +@pytest.mark.asyncio +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_formdata_request_body( + sentry_init, capture_events, capture_items, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + send_default_pii=True, + max_request_body_size="always", + integrations=[StarletteIntegration()], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) + + app = fastapi_app_factory() + client = TestClient(app) + + if span_streaming: + items = capture_items("event", "span") + + client.post( + "/body/form", + data=BODY_FORM.encode("utf-8"), + headers={ + "content-type": "multipart/form-data; boundary=fd721ef49ea403a6", + }, + ) + + (event,) = (item.payload for item in items if item.type == "event") + assert event["request"]["data"].keys() == PARSED_FORM.keys() + assert event["request"]["data"]["username"] == PARSED_FORM["username"] + assert event["request"]["data"]["password"] == "[Filtered]" + assert event["request"]["data"]["photo"] == "" + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + server_span = next( + span for span in spans if span["attributes"]["sentry.op"] == "http.server" + ) + + # Going forward, the sanitization of data will need to happen within the `before_send_span` hooks + # See https://sentry.slack.com/archives/C09RR0KD2N7/p1776951331206129?thread_ts=1776951227.440659&cid=C09RR0KD2N7 + parsed_form_attribute = json.loads( + server_span["attributes"][SPANDATA.HTTP_REQUEST_BODY_DATA] + ) + assert parsed_form_attribute.keys() == PARSED_FORM.keys() + assert parsed_form_attribute["username"] == PARSED_FORM["username"] + assert parsed_form_attribute["password"] == "hello123" + assert parsed_form_attribute["photo"] == "[Unparsable]" + else: + events = capture_events() + + client.post( + "/body/form", + data=BODY_FORM.encode("utf-8"), + headers={ + "content-type": "multipart/form-data; boundary=fd721ef49ea403a6", + }, + ) + + (event, transaction_event) = events + assert event["request"]["data"].keys() == PARSED_FORM.keys() + assert event["request"]["data"]["username"] == PARSED_FORM["username"] + assert event["request"]["data"]["password"] == "[Filtered]" + assert event["request"]["data"]["photo"] == "" + assert event["_meta"]["request"]["data"]["photo"] == { + "": {"rem": [["!raw", "x"]]} + } + + assert transaction_event["request"]["data"].keys() == PARSED_FORM.keys() + assert ( + transaction_event["request"]["data"]["username"] == PARSED_FORM["username"] + ) + assert transaction_event["request"]["data"]["password"] == "[Filtered]" + assert transaction_event["request"]["data"]["photo"] == "" + assert transaction_event["_meta"]["request"]["data"]["photo"] == { + "": {"rem": [["!raw", "x"]]} + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_request_body_too_big( + sentry_init, capture_events, capture_items, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + send_default_pii=True, + integrations=[StarletteIntegration()], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, + ) + + app = fastapi_app_factory() + client = TestClient(app) + + if span_streaming: + items = capture_items("event", "span") + + client.post( + "/body/form", + data=BODY_FORM.encode("utf-8"), + headers={ + "content-type": "multipart/form-data; boundary=fd721ef49ea403a6", + "cookie": "yummy_cookie=choco; tasty_cookie=strawberry", + }, + ) + + (event,) = (item.payload for item in items if item.type == "event") + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } + # Because request is too big only the AnnotatedValue is extracted. + assert event["_meta"]["request"]["data"] == {"": {"rem": [["!config", "x"]]}} + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + server_span = next( + span for span in spans if span["attributes"]["sentry.op"] == "http.server" + ) + + # Because request is too big only the AnnotatedValue is extracted. + assert ( + server_span["attributes"][SPANDATA.HTTP_REQUEST_BODY_DATA] + == "[Exceeds maximum size]" + ) + else: + events = capture_events() + + client.post( + "/body/form", + data=BODY_FORM.encode("utf-8"), + headers={ + "content-type": "multipart/form-data; boundary=fd721ef49ea403a6", + "cookie": "yummy_cookie=choco; tasty_cookie=strawberry", + }, + ) + + (event, transaction_event) = events + assert event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } + # Because request is too big only the AnnotatedValue is extracted. + assert event["_meta"]["request"]["data"] == {"": {"rem": [["!config", "x"]]}} + + assert transaction_event["request"]["cookies"] == { + "tasty_cookie": "strawberry", + "yummy_cookie": "choco", + } + # Because request is too big only the AnnotatedValue is extracted. + assert transaction_event["_meta"]["request"]["data"] == { + "": {"rem": [["!config", "x"]]} + } + + @pytest.mark.asyncio async def test_response(sentry_init, capture_events): # FastAPI is heavily based on Starlette so we also need @@ -246,142 +522,6 @@ def test_active_thread_id_span_streaming(sentry_init, capture_items, endpoint): assert str(data["active"]) == segments[0]["attributes"]["thread.id"] -def _post_body_fastapi_app(handler_awaitable): - app = FastAPI() - - @app.post("/body") - async def _route(request: Request): - await handler_awaitable(request) - return {"ok": True} - - return app - - -@pytest.mark.parametrize("middleware_spans", [False, True]) -def test_request_body_data_does_not_scrub_pii_span_streaming( - sentry_init, capture_items, middleware_spans -): - sentry_init( - auto_enabling_integrations=False, - integrations=[ - StarletteIntegration(middleware_spans=middleware_spans), - FastApiIntegration(middleware_spans=middleware_spans), - ], - traces_sample_rate=1.0, - _experiments={"trace_lifecycle": "stream"}, - ) - - async def _read_json(request): - await request.json() - - items = capture_items("span") - - client = TestClient(_post_body_fastapi_app(_read_json)) - response = client.post( - "/body", - json={ - "password": "ohno", - "authorization": "Bearer token", - "message": "hello", - }, - ) - assert response.status_code == 200 - - sentry_sdk.flush() - - segments = [item.payload for item in items if item.payload.get("is_segment")] - assert len(segments) == 1 - attr = segments[0]["attributes"]["http.request.body.data"] - - # Going forward, the sanitization of data will need to happen within the `before_send_span` hooks - # See https://sentry.slack.com/archives/C09RR0KD2N7/p1776951331206129?thread_ts=1776951227.440659&cid=C09RR0KD2N7 - assert "ohno" in attr - assert "Bearer token" in attr - assert "hello" in attr - - -@pytest.mark.skipif( - STARLETTE_VERSION < (0, 21), - reason="Requires Starlette >= 0.21, because earlier versions use a requests-based TestClient which does not support the 'content' kwarg", -) -@pytest.mark.parametrize("middleware_spans", [False, True]) -def test_request_body_data_annotated_value_top_level_span_streaming( - sentry_init, capture_items, middleware_spans -): - sentry_init( - auto_enabling_integrations=False, - integrations=[ - StarletteIntegration(middleware_spans=middleware_spans), - FastApiIntegration(middleware_spans=middleware_spans), - ], - traces_sample_rate=1.0, - _experiments={"trace_lifecycle": "stream"}, - ) - - async def _read_body(request): - await request.body() - - items = capture_items("span") - - client = TestClient(_post_body_fastapi_app(_read_body)) - response = client.post( - "/body", - content=b"not json and not form", - headers={"content-type": "application/octet-stream"}, - ) - assert response.status_code == 200 - - sentry_sdk.flush() - - segments = [item.payload for item in items if item.payload.get("is_segment")] - assert len(segments) == 1 - attr = segments[0]["attributes"]["http.request.body.data"] - - assert isinstance(attr, str) - assert attr == '""' - - -@pytest.mark.parametrize("middleware_spans", [False, True]) -def test_request_body_data_annotated_value_nested_span_streaming( - sentry_init, capture_items, middleware_spans -): - pytest.importorskip("multipart") - - sentry_init( - auto_enabling_integrations=False, - integrations=[ - StarletteIntegration(middleware_spans=middleware_spans), - FastApiIntegration(middleware_spans=middleware_spans), - ], - traces_sample_rate=1.0, - _experiments={"trace_lifecycle": "stream"}, - ) - - async def _read_form(request): - await request.form() - - items = capture_items("span") - - client = TestClient(_post_body_fastapi_app(_read_form)) - response = client.post( - "/body", - data={"name": "erica"}, - files={"avatar": ("photo.jpg", b"fake-bytes", "image/jpeg")}, - ) - assert response.status_code == 200 - - sentry_sdk.flush() - - segments = [item.payload for item in items if item.payload.get("is_segment")] - assert len(segments) == 1 - attr = segments[0]["attributes"]["http.request.body.data"] - - assert isinstance(attr, str) - parsed = json.loads(attr) - assert parsed["name"] == "erica" - assert "fake-bytes" not in attr - - @pytest.mark.parametrize("span_streaming", [True, False]) @pytest.mark.asyncio async def test_original_request_not_scrubbed( From 29fa27c801bee29767a43ecca06f88d663d61af8 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 18 May 2026 17:07:19 +0200 Subject: [PATCH 2/4] add test photo --- tests/integrations/fastapi/photo.jpg | Bin 0 -> 21014 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/integrations/fastapi/photo.jpg diff --git a/tests/integrations/fastapi/photo.jpg b/tests/integrations/fastapi/photo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..52fbeef721973389ab1d83fe7f81b511c07cb633 GIT binary patch literal 21014 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 R$o+56eGlcU$KL$w|Ji#B2Ydhk literal 0 HcmV?d00001 From 324fc68e93b3f1a66ae89e4d9e6669676f2c25b1 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 18 May 2026 17:22:17 +0200 Subject: [PATCH 3/4] update import --- tests/integrations/fastapi/test_fastapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 7a85a06392..8a3f608153 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -4,7 +4,6 @@ import os import threading import warnings -from typing import Annotated from unittest import mock import fastapi @@ -14,6 +13,7 @@ from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.testclient import TestClient from starlette.responses import JSONResponse +from typing_extensions import Annotated import sentry_sdk from sentry_sdk import capture_message From c9371020c63b991189af81df0e087551bf35ce5c Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Tue, 19 May 2026 08:54:44 +0200 Subject: [PATCH 4/4] fix parameter style --- tests/integrations/fastapi/test_fastapi.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 8a3f608153..bab57bb1b3 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -13,7 +13,6 @@ from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.testclient import TestClient from starlette.responses import JSONResponse -from typing_extensions import Annotated import sentry_sdk from sentry_sdk import capture_message @@ -104,9 +103,9 @@ async def body_json(payload: dict = Body(...)): @app.post("/body/form") async def body_form( - username: Annotated[str, Form()], - password: Annotated[str, Form()], - photo: Annotated[UploadFile, File()], + username: str = Form(...), + password: str = Form(...), + photo: UploadFile = File(...), ): capture_message("hi") return {"status": "ok"}