Skip to content

fix(serializer): Avoid building full repr of large objects (#6649) - #7176

Open
zkasuran wants to merge 1 commit into
getsentry:masterfrom
zkasuran:fix/logging-locals-serialization-eventloop-stall
Open

fix(serializer): Avoid building full repr of large objects (#6649)#7176
zkasuran wants to merge 1 commit into
getsentry:masterfrom
zkasuran:fix/logging-locals-serialization-eventloop-stall

Conversation

@zkasuran

@zkasuran zkasuran commented Aug 12, 2026

Copy link
Copy Markdown

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 trip UvicornWorker timeouts.

This adds bounded_repr() in utils.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 to repr(). Leaf values including strings are rendered in full, so string values are never shortened. The serializer uses it capped at max_value_length when that is set, otherwise at a generous default so only pathologically large graphs are cut.

Behavior for a user-supplied custom_repr is unchanged. The call site is repr_value or bounded_repr(value, ...), so a custom_repr result is returned verbatim exactly as before. bounded_repr only replaces the old safe_repr fallback when no custom_repr value is produced. The existing test_custom_repr and test_custom_repr_graceful_fallback_to_safe_repr still 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 check and ruff format --check clean on the changed files, mypy sentry_sdk with no new errors, plus a before/after on the reporter's FastAPI repro dropping the stall from over a second to milliseconds.

…#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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread sentry_sdk/utils.py
@@ -1,5 +1,6 @@
import base64
import copy
import dataclasses

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 73f8f54. Configure here.

Comment thread sentry_sdk/utils.py
if field.repr
)
_render_items(type(obj).__qualname__ + "(", ")", items, depth)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 73f8f54. Configure here.

Comment thread sentry_sdk/utils.py
"""Raised internally by bounded_repr() once the length budget is spent."""


def bounded_repr(value: "Any", max_length: "Optional[int]") -> str:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@zkasuran

Copy link
Copy Markdown
Author

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 dataclasses import breaking 3.6, plus dataclass __repr__/repr=False overrides being ignored. Both fall out of rendering the repr ourselves rather than calling it.

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 __repr__ recursion (the nested-router tree), not in the serializer's traversal, so the serializer's existing depth limit never sees it. I checked stdlib reprlib as the one option that isn't our own logic: for a recursive dataclass it still calls the object's full __repr__ and only truncates the result, so it changes output without avoiding the cost.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Logging integration blocks the event loop for seconds while serializing frame locals on FastAPI >= 0.137

2 participants