Skip to content

Commit 3bbd78d

Browse files
fix(fastapi): Stop eagerly consuming request bodies for streamed spans (#6286)
Only attach cached request bodies to streamed spans to avoid eagerly consuming the request body. Use the `_json` and `_form` attributes instead of `json()` and `form()` accessors so the SDK does not consume the body. If neither `_json` nor `_form` exists, either the request body is not JSON/FormData or the endpoint did not yet access the request body. In that case, omit the request body attribute.
1 parent fa67e36 commit 3bbd78d

3 files changed

Lines changed: 248 additions & 258 deletions

File tree

sentry_sdk/integrations/fastapi.py

Lines changed: 65 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,24 @@
44
from typing import TYPE_CHECKING
55

66
import sentry_sdk
7+
from sentry_sdk.consts import SPANDATA
78
from sentry_sdk.integrations import DidNotEnable
89
from sentry_sdk.scope import should_send_default_pii
9-
from sentry_sdk.traces import StreamedSpan
10+
from sentry_sdk.traces import StreamedSpan, get_current_span
1011
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
1112
from sentry_sdk.tracing_utils import has_span_streaming_enabled
1213
from sentry_sdk.utils import transaction_from_function
1314

1415
if TYPE_CHECKING:
15-
from typing import Any, Callable, Dict
16+
from typing import Any, Awaitable, Callable, Dict
1617

1718
from sentry_sdk._types import Event
1819

1920
try:
2021
from sentry_sdk.integrations.starlette import (
2122
StarletteIntegration,
2223
StarletteRequestExtractor,
23-
_set_request_body_data_on_streaming_segment,
24+
_get_cached_request_body_attribute,
2425
)
2526
except DidNotEnable:
2627
raise DidNotEnable("Starlette is not installed")
@@ -75,6 +76,66 @@ def _set_transaction_name_and_source(
7576
scope.set_transaction_name(name, source=source)
7677

7778

79+
async def _wrap_async_handler(
80+
handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any"
81+
) -> "Any":
82+
"""
83+
Wraps an asynchronous handler function to attach request info to errors and the server segment span.
84+
The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event
85+
processor can still cause application hangs.
86+
"""
87+
client = sentry_sdk.get_client()
88+
integration = client.get_integration(FastApiIntegration)
89+
if integration is None:
90+
return await handler(*args, **kwargs)
91+
92+
request = args[0]
93+
94+
_set_transaction_name_and_source(
95+
sentry_sdk.get_current_scope(), integration.transaction_style, request
96+
)
97+
sentry_scope = sentry_sdk.get_isolation_scope()
98+
extractor = StarletteRequestExtractor(request)
99+
info = await extractor.extract_request_info()
100+
101+
def _make_request_event_processor(
102+
req: "Any", integration: "Any"
103+
) -> "Callable[[Event, Dict[str, Any]], Event]":
104+
def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event":
105+
# Extract information from request
106+
request_info = event.get("request", {})
107+
if info:
108+
if "cookies" in info and should_send_default_pii():
109+
request_info["cookies"] = info["cookies"]
110+
if "data" in info:
111+
request_info["data"] = info["data"]
112+
event["request"] = deepcopy(request_info)
113+
114+
return event
115+
116+
return event_processor
117+
118+
sentry_scope._name = FastApiIntegration.identifier
119+
sentry_scope.add_event_processor(
120+
_make_request_event_processor(request, integration)
121+
)
122+
123+
try:
124+
return await handler(*args, **kwargs)
125+
finally:
126+
current_span = get_current_span()
127+
128+
if type(current_span) is StreamedSpan:
129+
request_body = _get_cached_request_body_attribute(
130+
client=client, request=request
131+
)
132+
if request_body:
133+
current_span._segment.set_attribute(
134+
SPANDATA.HTTP_REQUEST_BODY_DATA,
135+
request_body,
136+
)
137+
138+
78139
def patch_get_request_handler() -> None:
79140
old_get_request_handler = fastapi.routing.get_request_handler
80141

@@ -113,46 +174,7 @@ def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any":
113174
old_app = old_get_request_handler(*args, **kwargs)
114175

115176
async def _sentry_app(*args: "Any", **kwargs: "Any") -> "Any":
116-
client = sentry_sdk.get_client()
117-
integration = client.get_integration(FastApiIntegration)
118-
if integration is None:
119-
return await old_app(*args, **kwargs)
120-
121-
request = args[0]
122-
123-
_set_transaction_name_and_source(
124-
sentry_sdk.get_current_scope(), integration.transaction_style, request
125-
)
126-
sentry_scope = sentry_sdk.get_isolation_scope()
127-
extractor = StarletteRequestExtractor(request)
128-
info = await extractor.extract_request_info()
129-
130-
def _make_request_event_processor(
131-
req: "Any", integration: "Any"
132-
) -> "Callable[[Event, Dict[str, Any]], Event]":
133-
def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event":
134-
# Extract information from request
135-
request_info = event.get("request", {})
136-
if info:
137-
if "cookies" in info and should_send_default_pii():
138-
request_info["cookies"] = info["cookies"]
139-
if "data" in info:
140-
request_info["data"] = info["data"]
141-
event["request"] = deepcopy(request_info)
142-
143-
return event
144-
145-
return event_processor
146-
147-
sentry_scope._name = FastApiIntegration.identifier
148-
sentry_scope.add_event_processor(
149-
_make_request_event_processor(request, integration)
150-
)
151-
152-
if has_span_streaming_enabled(client.options):
153-
_set_request_body_data_on_streaming_segment(info)
154-
155-
return await old_app(*args, **kwargs)
177+
return await _wrap_async_handler(old_app, *args, **kwargs)
156178

157179
return _sentry_app
158180

sentry_sdk/integrations/starlette.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -254,18 +254,6 @@ def _default(value: "Any") -> "Any":
254254
return json.dumps(data, default=_default)
255255

256256

257-
def _set_request_body_data_on_streaming_segment(
258-
info: "Optional[Dict[str, Any]]",
259-
) -> None:
260-
current_span = get_current_span()
261-
if info and "data" in info and type(current_span) is StreamedSpan:
262-
with capture_internal_exceptions():
263-
current_span._segment.set_attribute(
264-
"http.request.body.data",
265-
_serialize_request_body_data(info["data"]),
266-
)
267-
268-
269257
@ensure_integration_enabled(StarletteIntegration)
270258
def _capture_exception(exception: BaseException, handled: "Any" = False) -> None:
271259
event, hint = event_from_exception(

0 commit comments

Comments
 (0)