|
8 | 8 | from typing import TYPE_CHECKING |
9 | 9 |
|
10 | 10 | import sentry_sdk |
11 | | -from sentry_sdk.consts import OP |
| 11 | +from sentry_sdk._types import OVER_SIZE_LIMIT_SUBSTITUTE |
| 12 | +from sentry_sdk.consts import OP, SPANDATA |
12 | 13 | from sentry_sdk.integrations import ( |
13 | 14 | _DEFAULT_FAILED_REQUEST_STATUS_CODES, |
14 | 15 | DidNotEnable, |
|
46 | 47 | import starlette # type: ignore |
47 | 48 | from starlette import __version__ as STARLETTE_VERSION |
48 | 49 | from starlette.applications import Starlette # type: ignore |
49 | | - from starlette.datastructures import UploadFile # type: ignore |
| 50 | + from starlette.datastructures import ( # type: ignore |
| 51 | + UploadFile, |
| 52 | + ) |
50 | 53 | from starlette.middleware import Middleware # type: ignore |
51 | 54 | from starlette.middleware.authentication import ( # type: ignore |
52 | 55 | AuthenticationMiddleware, |
@@ -478,61 +481,113 @@ def _is_async_callable(obj: "Any") -> bool: |
478 | 481 | ) |
479 | 482 |
|
480 | 483 |
|
481 | | -def patch_request_response() -> None: |
482 | | - old_request_response = starlette.routing.request_response |
| 484 | +def _get_cached_request_body_attribute( |
| 485 | + client: "sentry_sdk.client.BaseClient", request: "Request" |
| 486 | +) -> "Optional[str]": |
| 487 | + """ |
| 488 | + Returns a stringified JSON representation of the request body if the request body is cached and within size bounds. |
| 489 | + """ |
| 490 | + if "content-length" not in request.headers: |
| 491 | + return None |
483 | 492 |
|
484 | | - def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": |
485 | | - old_func = func |
| 493 | + try: |
| 494 | + content_length = int(request.headers["content-length"]) |
| 495 | + except ValueError: |
| 496 | + return None |
486 | 497 |
|
487 | | - is_coroutine = _is_async_callable(old_func) |
488 | | - if is_coroutine: |
| 498 | + if content_length and not request_body_within_bounds(client, content_length): |
| 499 | + return OVER_SIZE_LIMIT_SUBSTITUTE |
489 | 500 |
|
490 | | - async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": |
491 | | - client = sentry_sdk.get_client() |
492 | | - integration = client.get_integration(StarletteIntegration) |
493 | | - if integration is None: |
494 | | - return await old_func(*args, **kwargs) |
| 501 | + if hasattr(request, "_json"): |
| 502 | + return json.dumps(request._json) |
495 | 503 |
|
496 | | - request = args[0] |
| 504 | + formdata_body = getattr(request, "_form", None) |
| 505 | + if formdata_body is None: |
| 506 | + return None |
497 | 507 |
|
498 | | - _set_transaction_name_and_source( |
499 | | - sentry_sdk.get_current_scope(), |
500 | | - integration.transaction_style, |
501 | | - request, |
502 | | - ) |
| 508 | + form_data = {} |
| 509 | + for key, val in formdata_body.items(): |
| 510 | + is_file = isinstance(val, UploadFile) |
| 511 | + form_data[key] = val if not is_file else "[Unparsable]" |
503 | 512 |
|
504 | | - sentry_scope = sentry_sdk.get_isolation_scope() |
505 | | - extractor = StarletteRequestExtractor(request) |
506 | | - info = await extractor.extract_request_info() |
| 513 | + return json.dumps(form_data) |
507 | 514 |
|
508 | | - def _make_request_event_processor( |
509 | | - req: "Any", integration: "Any" |
510 | | - ) -> "Callable[[Event, dict[str, Any]], Event]": |
511 | | - def event_processor( |
512 | | - event: "Event", hint: "Dict[str, Any]" |
513 | | - ) -> "Event": |
514 | | - # Add info from request to event |
515 | | - request_info = event.get("request", {}) |
516 | | - if info: |
517 | | - if "cookies" in info: |
518 | | - request_info["cookies"] = info["cookies"] |
519 | | - if "data" in info: |
520 | | - request_info["data"] = info["data"] |
521 | | - event["request"] = deepcopy(request_info) |
522 | 515 |
|
523 | | - return event |
| 516 | +async def _wrap_async_handler( |
| 517 | + handler: "Callable[..., Awaitable[Any]]", *args: "Any", **kwargs: "Any" |
| 518 | +) -> "Any": |
| 519 | + """ |
| 520 | + Wraps an asynchronous handler function to attach request info to errors and the server segment span. |
| 521 | + The request body cached on the Starlette Request object is attached to streamed spans, but consuming the request body in the event |
| 522 | + processor can still cause application hangs. |
| 523 | + """ |
| 524 | + client = sentry_sdk.get_client() |
| 525 | + integration = client.get_integration(StarletteIntegration) |
| 526 | + if integration is None: |
| 527 | + return await handler(*args, **kwargs) |
524 | 528 |
|
525 | | - return event_processor |
| 529 | + request = args[0] |
526 | 530 |
|
527 | | - sentry_scope._name = StarletteIntegration.identifier |
528 | | - sentry_scope.add_event_processor( |
529 | | - _make_request_event_processor(request, integration) |
| 531 | + _set_transaction_name_and_source( |
| 532 | + sentry_sdk.get_current_scope(), |
| 533 | + integration.transaction_style, |
| 534 | + request, |
| 535 | + ) |
| 536 | + |
| 537 | + sentry_scope = sentry_sdk.get_isolation_scope() |
| 538 | + extractor = StarletteRequestExtractor(request) |
| 539 | + |
| 540 | + info = await extractor.extract_request_info() |
| 541 | + |
| 542 | + def _make_request_event_processor( |
| 543 | + req: "Any", integration: "Any" |
| 544 | + ) -> "Callable[[Event, dict[str, Any]], Event]": |
| 545 | + def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": |
| 546 | + # Add info from request to event |
| 547 | + request_info = event.get("request", {}) |
| 548 | + if info: |
| 549 | + if "cookies" in info: |
| 550 | + request_info["cookies"] = info["cookies"] |
| 551 | + if "data" in info: |
| 552 | + request_info["data"] = info["data"] |
| 553 | + event["request"] = deepcopy(request_info) |
| 554 | + |
| 555 | + return event |
| 556 | + |
| 557 | + return event_processor |
| 558 | + |
| 559 | + sentry_scope._name = StarletteIntegration.identifier |
| 560 | + sentry_scope.add_event_processor( |
| 561 | + _make_request_event_processor(request, integration) |
| 562 | + ) |
| 563 | + |
| 564 | + try: |
| 565 | + return await handler(*args, **kwargs) |
| 566 | + finally: |
| 567 | + current_span = get_current_span() |
| 568 | + |
| 569 | + if type(current_span) is StreamedSpan: |
| 570 | + request_body = _get_cached_request_body_attribute( |
| 571 | + client=client, request=request |
| 572 | + ) |
| 573 | + if request_body: |
| 574 | + current_span._segment.set_attribute( |
| 575 | + SPANDATA.HTTP_REQUEST_BODY_DATA, |
| 576 | + request_body, |
530 | 577 | ) |
531 | 578 |
|
532 | | - if has_span_streaming_enabled(client.options): |
533 | | - _set_request_body_data_on_streaming_segment(info) |
534 | 579 |
|
535 | | - return await old_func(*args, **kwargs) |
| 580 | +def patch_request_response() -> None: |
| 581 | + old_request_response = starlette.routing.request_response |
| 582 | + |
| 583 | + def _sentry_request_response(func: "Callable[[Any], Any]") -> "ASGIApp": |
| 584 | + old_func = func |
| 585 | + |
| 586 | + is_coroutine = _is_async_callable(old_func) |
| 587 | + if is_coroutine: |
| 588 | + |
| 589 | + async def _sentry_async_func(*args: "Any", **kwargs: "Any") -> "Any": |
| 590 | + return await _wrap_async_handler(old_func, *args, **kwargs) |
536 | 591 |
|
537 | 592 | func = _sentry_async_func |
538 | 593 |
|
|
0 commit comments