Skip to content

Report FastAPI and FastMCP errors without StarletteIntegration while tracing is off - #362

Merged
lesnik512 merged 3 commits into
mainfrom
lean-starlette-capture
Oct 28, 2026
Merged

lesnik512 merged 3 commits into
mainfrom
lean-starlette-capture

Conversation

@lesnik512

@lesnik512 lesnik512 commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Closes #341.

While Sentry tracing is off, FastAPI and FastMCP now report request errors through lite-bootstrap's own middleware instead of sentry-sdk's StarletteIntegration (and FastApiIntegration on FastAPI). With tracing off, those integrations still build and discard a transaction on every request. This is the Starlette half of #307, which #361 did for Litestar. #313 comes next.

Changes

  • SentryLeanCaptureConfig in sentry_instrument.py now holds sentry_lean_error_capture, its resolution rule and the two config warnings that feat: capture Litestar errors without LitestarIntegration while Sentry tracing is off #361 put on LitestarConfig. LitestarConfig, FastAPIConfig and FastMcpConfig inherit it. Each one names the integrations it replaces in a _sentry_lean_replaced_integrations ClassVar, which is what the "supplied integration keeps the integration" rule and the warning text read.

  • Shared helpers in sentry_instrument.py, used by all three frameworks:

    • capture_request_exception, which passes client_options on 2.x only
    • read_sentry_trace_headers
    • disable_sentry_integrations
    • warn_about_sentry_integrations_installed_earlier
    • SENTRY_FAILED_REQUEST_STATUS_CODES

    The Litestar code now uses them, with no change in behaviour.

  • lite_bootstrap/helpers/starlette_sentry.py holds StarletteSentryCaptureMiddleware, a pure-ASGI middleware for HTTP and WebSocket requests. For each request it opens an isolation scope and a request session, clears breadcrumbs, and continues sentry-trace and baggage. It also adds an event processor and reports any exception that escapes. import_checker.is_starlette_installed guards the module.

  • FastAPISentryInstrument disables both integrations and adds the middleware. FastMcpSentryInstrument disables StarletteIntegration and wraps every http_app() through _postprocess_http_apps, as the OTel and Prometheus instruments do, and its teardown restores http_app. MCPIntegration is not touched.

What gets reported

The rules follow StarletteIntegration:

  • An exception that escapes the application is reported, handled: False.
  • An exception that an exception handler answers is reported, handled: True, only when it carries an int status_code from 500 to 599. So HTTPException(503) is reported, HTTPException(404) is not, and a custom exception that a handler turns into a 503 is not reported unless the exception itself has that status_code.

Starlette offers no hook for the second rule. The integration patches ExceptionMiddleware.__init__ for every app in the process. The lean middleware instead finds the ExceptionMiddleware below it when Starlette builds the stack, by following .app, and wraps that one instance's _exception_handlers. The stack is built on the first request, so handlers added after bootstrap are included. Each wrapper keeps Starlette's sync/async split, so sync handlers still run in the threadpool. A middleware added before bootstrap that keeps its inner app anywhere but .app hides ExceptionMiddleware, and then only escaping exceptions are reported. A test pins that, and the docs say so.

Request data

Request data is gated by data_collection, as it is for Litestar:

  • _get_request_data (with _RootPathInPath.EITHER, as StarletteIntegration passes on Starlette 0.33 and later) supplies the method, URL, query string and headers, plus the client address when user_info is on.
  • Cookies go through _apply_key_value_collection_filtering.
  • The user comes from scope["user"] (username, id, email) when user_info is on.
  • The transaction is scope["route"].path with source route. A transaction name already on the event wins.
  • Bodies are covered under Departures.

Departures from the issue

  • Bodies come from a copy of what the application read, not from a Request object. Starlette caches a read body on the Request instance, which a middleware never sees. So when http_bodies includes "incoming_request", the middleware wraps receive and keeps the chunks of http.request messages. A body is attached only once the application has read all of it. With the default http_bodies=[], receive is not wrapped. JSON and URL-encoded bodies are attached parsed, multipart becomes AnnotatedValue.removed_because_raw_data(), and anything else, including JSON that fails to parse, is attached as text, as the Litestar path does. 2.x's integration annotates raw bodies instead of attaching them. Neither path checks max_request_body_size.
  • The test fixture moved to conftest.py. It removes the framework integrations from sentry-sdk's process-wide registries and now covers litestar, starlette and fastapi for every test. Without it, test_fastapi_bootstrap.py saw the new "installed by an earlier init" warning. Clearing the registries makes sentry-sdk run setup_once again, which wraps the framework once more each time; across the suite that stacked 126 wrappers on Starlette.__call__ and overflowed the stack on Python 3.11 in CI. So once an integration has been set up, the fixture replaces its setup_once with a no-op for later tests, and the patches from the first run stay in place. A second autouse fixture gives each test fresh current and isolation scopes: an in-memory FastMCP client test left a tools/call ... transaction name on the main context's scope, and every later event picked it up.
  • No rerun of benchmarks/sentry_scenarios.py. It calls sentry_sdk.init directly, so lean capture cannot change its numbers. The FastAPI and FastMCP suites measure lite-bootstrap itself, and they were rerun instead (below).
  • Granian is not a test dependency. uvicorn already comes in through fastmcp, so the "one event per failing request" test runs a real uvicorn server, with lean capture on and off. I checked Granian by hand with the same app: one event in both modes. sentry-sdk's dedupe integration drops the server's log event because it carries the same exception object.

Correction to #361

The #361 description said sentry_nointegration "disables the integration and adds nothing else". That was wrong: the scenario left sentry_lean_error_capture at its default, so lean capture was still on. It now also sets sentry_lean_error_capture=False, and the same scenario added for FastAPI and FastMCP means no request capture at all. Remeasured on Litestar: sentry +6.7 / +6.8 µs and sentry_nointegration +0.3 / +0.0 µs (2.73.0 / 3.0.0b2). So Litestar's lean middleware costs about 6.7 µs, not "almost nothing". benchmarks/README.md §8b records the rerun.

Tests

tests/test_starlette_sentry_capture.py was written first. Against main's code, 15 of its 36 tests failed, among them:

  • test_lean_capture_is_the_default_on_fastapi_while_tracing_is_off and its FastMCP twin: starlette and fastapi were active.
  • the body cases: on 2.73 the integration read a body the handler never touched, and attached "" for a raw body.
  • every case that sets sentry_lean_error_capture.

The rest passed on main, which shows lean capture matches the integration on those points:

  • request data, cookies, user and client address
  • trace continuation
  • 4xx skipped and 5xx HTTPException reported
  • breadcrumb isolation
  • WebSocket errors
  • one event under uvicorn

New tests cover:

  • a handler registered after bootstrap
  • a functools.partial handler
  • a sync handler run in a worker thread
  • an inner middleware that hides .app
  • a failing FastMCP tool call giving one event with the request attached, in both modes
  • teardown restoring http_app

test_fastapi_bootstrapper_runs_without_sentry_sdk and test_fastmcp_bootstrapper_runs_without_sentry_sdk reload each module with sentry-sdk missing, as #361's Litestar test does.

The full suite passes, 537 tests at 100% coverage, on sentry-sdk 2.73.0 and 3.0.0b2. ty passes against both, and so do just lint, just adr-check, just docs-build and lychee.

The floors steps pass locally for fastapi and fastmcp on 3.11 and 3.14 (sentry-sdk 2.70.0, fastapi 0.133.0, fastmcp 3.0.0, starlette 1.10.0). floor_smoke.py now sends one failing request through lean capture for each, without a test client, because the floors don't install httpx. The FastMCP request runs in its own Sentry-only bootstrap. The full one exports metrics to an absent collector and already takes about 65 s at this floor on main.

CI's floors only cover fastapi-all, which raises fastapi to 0.133, so I checked the fastapi-sentry floor by hand: fastapi 0.112.2, starlette 0.38.6, sentry-sdk 2.70.0. There the transaction is /items/{item_id} and HTTPException(503) is reported as handled.

Measurement

In-process on an M2 Mac (CPython 3.14.7, fastapi 0.162.0, fastmcp 4.1.4, starlette 1.10.0), in µs over the same app without Sentry:

scenario FastAPI 2.73.0 FastAPI 3.0.0b2 FastMCP 2.73.0 FastMCP 3.0.0b2
sentry (lean, the new default) +6.1 +5.1 +20.6 +15.9
sentry_sampled_trace +11.5 +10.9 +16.1 +18.9
sentry_nointegration (no capture) +0.1 -0.2 -0.3 +1.8
sentry_integration (sentry_lean_error_capture=False) +57.1 +47.0 +82.2 +65.0
sentry_integration_sampled_trace +59.2 +113.2 +82.3 +216.4

FastMCP is per tools/call over a base of about 620 µs, and its rounds differed by up to 15%. The FastAPI row with the integration and a sampled trace on 3.0.0b2 varied by 40% between rounds.

Docs

  • docs/integrations/fastapi.md and fastmcp.md get a Sentry section: the rule, what is reported, the request data, the ExceptionMiddleware lookup and its limit, uvicorn and Granian, and the numbers.
  • configuration.md: the sentry_lean_error_capture bullet names all three frameworks. The "second most expensive instrument" paragraph is now in the past tense.
  • performance.md: the "savings applied for you" paragraph lists lean capture, and the FastAPI and FastMCP sections give the 2.0 numbers.
  • benchmarks/README.md: rows measured with the old defaults are relabelled "1.x defaults", and §4d, §8b and §8e get the rerun. It also notes that sentry_noauto no longer stops request capture, which Default sentry_auto_enabling_integrations to False together with lean capture #313 relies on.

Breaking changes

These apply to FastAPI and FastMCP services with a sentry_dsn and no tracing option.

  • No request transactions are created.
  • A request body is attached only when http_bodies includes "incoming_request" and the handler read all of it. On 2.x the integration read the body before the handler ran.
  • A raw (non-JSON, non-form) body is attached as text instead of a "raw data" annotation.

Set sentry_lean_error_capture=False to keep the integrations.

…tracing is off

While Sentry tracing is off, FastAPIBootstrapper and FastMcpBootstrapper disable
sentry-sdk's StarletteIntegration (and FastApiIntegration on FastAPI) and report
request errors through a pure-ASGI middleware instead, with the same
sentry_lean_error_capture rule Litestar got in #361. The rule now lives in
SentryLeanCaptureConfig, shared by all three configs.

Closes #341.
…y test

Clearing sentry-sdk's processed-integrations registry before each test made
StarletteIntegration.setup_once run again for every bootstrap, and each run
wraps Starlette.__call__ once more. By the end of the suite 126 wrappers were
stacked, enough to overflow the stack on Python 3.11 in CI. Once an
integration has been set up, the fixture now replaces its setup_once with a
no-op, so the registries still read as a fresh process without patching twice.
…hutdown

test_teardown_removes_filters_even_when_a_handler_close_fails added a handler
whose close always raised and left it on the root logger. Its exception kept
it alive until the cyclic garbage collector ran, so whether Litestar's
dictConfig in a later test closed it again, and failed, depended on GC timing.
The handler now raises on its first close only, and the test detaches it.
@lesnik512
lesnik512 merged commit 4e4e02e into main Oct 28, 2026
37 checks passed
@lesnik512
lesnik512 deleted the lean-starlette-capture branch October 28, 2026 20:27
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.

Capture Sentry errors without the Starlette integration when Sentry tracing is off (FastAPI, FastMCP)

1 participant