fix(serializer): Avoid building full repr of large objects (#6649) - #7176
fix(serializer): Avoid building full repr of large objects (#6649)#7176zkasuran wants to merge 1 commit into
Conversation
…#6649) When capturing frame locals, the serializer builds repr() of any non-container object in full and only truncates the resulting string afterwards. repr() of a large container or dataclass walks the whole object graph, so the string is built entirely and then mostly thrown away. On FastAPI >= 0.137 every nested routing frame holds an _IncludedRouter, a dataclass whose auto-generated __repr__ recurses through the full router tree. Serializing one frame local turned into a multi-megabyte repr. The same object appears in each nested frame, so logging a single exception blocked the event loop for hundreds of milliseconds to seconds and could trip gunicorn UvicornWorker timeouts. Add bounded_repr(), which renders dataclass fields and the standard container types itself and stops once the output reaches the limit, returning a prefix of repr() marked with "...". When the full repr fits the result is identical to repr(). Leaf values including strings are rendered in full, so string values are never shortened. The serializer now uses it, capped at max_value_length when set and otherwise at a generous default so only pathologically large graphs are cut.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 73f8f54. Configure here.
| @@ -1,5 +1,6 @@ | |||
| import base64 | |||
| import copy | |||
| import dataclasses | |||
There was a problem hiding this comment.
Dataclasses import breaks Python 3.6
High Severity
Unconditional import dataclasses makes sentry_sdk.utils fail to import on Python 3.6. That module is not in the standard library there, and dataclasses is only a test dependency, not an install_requires entry, so production 3.6 installs will fail on import sentry_sdk.
Reviewed by Cursor Bugbot for commit 73f8f54. Configure here.
| if field.repr | ||
| ) | ||
| _render_items(type(obj).__qualname__ + "(", ")", items, depth) | ||
| return |
There was a problem hiding this comment.
Dataclass repr overrides ignored
High Severity
bounded_repr treats every dataclass instance as if it used the default field repr. Class-level repr=False and custom __repr__ methods are skipped, so values that were intentionally omitted or redacted can appear in serialized frame locals.
Reviewed by Cursor Bugbot for commit 73f8f54. Configure here.
| """Raised internally by bounded_repr() once the length budget is spent.""" | ||
|
|
||
|
|
||
| def bounded_repr(value: "Any", max_length: "Optional[int]") -> str: |
There was a problem hiding this comment.
We can't do this in a minor version.
The repr() implementation for built-in types is a Python implementation detail, and enforcing our own logic is a disruptive change.
|
Makes sense, thanks for the quick look. Agreed that reimplementing repr for built-in types is too disruptive for a minor release. The two Bugbot points are fair too: the unconditional I did try to find a non-disruptive way to bound the cost without reimplementing repr. The catch in this issue is that the expense lives inside the object's own Given that, I'll leave the reasonable-limits design to you rather than push this approach. Happy to test a candidate against the repro in the issue if that helps. |


When capturing frame locals, the serializer builds
repr()of any non-container object in full and only truncates the resulting string afterwards.repr()of a large container or dataclass walks the whole object graph, so the string is built entirely and then mostly discarded. On FastAPI >= 0.137 every nested routing frame holds an_IncludedRouter, a dataclass whose auto-generated__repr__recurses through the full router tree, so serializing one frame local becomes a multi-megabyte repr. The same object recurs in each nested frame, so logging a single exception blocks the event loop for hundreds of milliseconds to seconds and can tripUvicornWorkertimeouts.This adds
bounded_repr()inutils.py, which renders dataclass fields and the standard container types itself and stops once the output reaches the limit, returning a prefix marked with.... When the full repr fits, the result is identical torepr(). Leaf values including strings are rendered in full, so string values are never shortened. The serializer uses it capped atmax_value_lengthwhen that is set, otherwise at a generous default so only pathologically large graphs are cut.Behavior for a user-supplied
custom_repris unchanged. The call site isrepr_value or bounded_repr(value, ...), so acustom_reprresult is returned verbatim exactly as before.bounded_repronly replaces the oldsafe_reprfallback when nocustom_reprvalue is produced. The existingtest_custom_reprandtest_custom_repr_graceful_fallback_to_safe_reprstill pass.Fixes #6649
AI assistance (Claude, Anthropic) was used in developing this change. The design, review and verification were done by the author. Verified locally on 3.12: the serializer, utils and logging tests pass (230), the new tests failing before and passing after the fix,
ruff checkandruff format --checkclean on the changed files,mypy sentry_sdkwith no new errors, plus a before/after on the reporter's FastAPI repro dropping the stall from over a second to milliseconds.