Skip to content

fix(fastapi): Prevent double wrapping of sync handlers on FastAPI >= 0.137#6569

Merged
alexander-alderman-webb merged 2 commits into
getsentry:masterfrom
jhonny-on:fix/fastapi-sync-handler-recursion
Jun 15, 2026
Merged

fix(fastapi): Prevent double wrapping of sync handlers on FastAPI >= 0.137#6569
alexander-alderman-webb merged 2 commits into
getsentry:masterfrom
jhonny-on:fix/fastapi-sync-handler-recursion

Conversation

@jhonny-on

@jhonny-on jhonny-on commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #6568

What

patch_get_request_handler() wraps dependant.call in a _sentry_call closure and writes it back to the shared dependant object. In FastAPI ≤ 0.136 this was called once per route at registration time, so one wrapper was applied and never repeated. In FastAPI 0.137, include_router() preserves a router tree instead of flattening routes, causing get_request_handler() to be called on every request — so the wrapper count grows by 1 per request, until Python's 1000-frame recursion limit is hit at ~987 requests and the endpoint returns 500 permanently.

Only def (sync) endpoints are affected. async def endpoints go through _sentry_app, not _sentry_call, and are unaffected.

Why the root cause is correct

Test proves it

test_sync_endpoint_does_not_crash_after_many_requests sends 1,000 requests to a sync endpoint registered via include_router() and asserts all return 200. With the bug it fails at request 987; with the fix all 1,000 pass.

Note: the test takes ~75 seconds because it sends 1,000 real requests through TestClient. Happy to discuss a faster approach if the team prefers (e.g. inspecting dependant.call directly, or parameterising the request count).

Would not reccommed to merge this test as-is

Additionally, the diagnosis was confirmed by local reproduction before filing the issue:

  • FastAPI 0.137.0 + sentry-sdk 2.35.2, real production app (80+ include_router() calls), 1,200 rapid-fire TestClient requests
  • **Crashed at request 987 **
  • Time was irrelevant; request count was the trigger
  • The same test with FastAPI 0.136.3 runs all 1,200 requests without issue

How

Add a _sentry_is_patched = True sentinel attribute to _sentry_call after wrapping, and check for it before wrapping. If dependant.call is already patched, skip — the _sentry_call still executes on every request (thread/profiling context updates still happen), it just isn't re-wrapped.

# before
dependant.call = _sentry_call

# after
_sentry_call._sentry_is_patched = True
dependant.call = _sentry_call

# and in the guard condition:
and not getattr(dependant.call, "_sentry_is_patched", False)

Disclosure: This fix was developed with AI assistance (Claude) and has been manually reviewed, reproduced, and verified by a human engineer before submission.

…ts with FastAPI >= 0.137

FastAPI 0.137 changed include_router() to preserve a router tree instead
of flattening routes. This causes get_request_handler() to be called on
every request rather than once at registration. patch_get_request_handler()
mutates dependant.call in-place, accumulating one _sentry_call wrapper per
request. After ~987 requests Python's 1000-frame recursion limit is hit and
the sync endpoint returns 500 permanently.

Fix: add a _sentry_is_patched sentinel to _sentry_call so subsequent calls
to the patched factory skip re-wrapping an already-patched dependant.

Fixes getsentry#6568

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jhonny-on jhonny-on marked this pull request as ready for review June 15, 2026 16:17
@jhonny-on jhonny-on requested a review from a team as a code owner June 15, 2026 16:17
Comment thread sentry_sdk/integrations/fastapi.py
@alexander-alderman-webb alexander-alderman-webb changed the title fix(fastapi): guard against _sentry_call accumulation on sync endpoints with FastAPI >= 0.137 Jun 15, 2026
@alexander-alderman-webb alexander-alderman-webb merged commit 6bcfb9c into getsentry:master Jun 15, 2026
139 checks passed
masonpetrosky added a commit to masonpetrosky/fantasy-foundry that referenced this pull request Jun 17, 2026
…ler wrapper leak

sentry-sdk <2.63.0 re-wraps a route's dependant.call in _sentry_call on every
request under fastapi>=0.137 (no double-wrap guard, sync handlers only); the
chain grows ~1 frame/request until it exceeds Python's recursion limit, 500ing
every sync server-rendered page (/, /player/<slug>, ...) once the long-running
process has served enough requests. A restart hid it, so it recurred ~daily.

Upstream fix getsentry/sentry-python#6569 ("Prevent double wrapping of sync
handlers on FastAPI >= 0.137"). Reproduced locally: on 2.62.0 a sync SSR route
500s at the ~988th request (matches the prod traceback's "[Previous line
repeated 988 more times]"); on 2.63.0 it survives indefinitely.

Adds a regression guard (sentry-sdk >= 2.63.0 while on fastapi >= 0.137) and
syncs the project-summary backend pytest file count (211 -> 214).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants