getsentry / getsentry/sentry-python

StarletteIntegration eagerly drains the ASGI receive channel for request bodies under max_request_body_size, breaking handlers that read the body themselves afterward

未關閉
#7,285 3 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視
Bug Integration: Starlette/FastAPI Python
主要語言
Python
星號
2.2k
分支
669
平均合併
1 天 40 分鐘
30 天內合併 PR
212

描述

**Summary**

`StarletteIntegration.patch_request_response()` wraps every Starlette route endpoint. Before the real endpoint runs, `StarletteRequestExtractor.extract_request_info()` calls `await self.request.body()` (via `.form()`) whenever `content_length` is within `max_request_body_size` (default `"medium"` → `content_length <= 10_000` bytes, see `request_body_within_bounds()` in `_wsgi_common.py`). That fully drains the request's raw ASGI `receive()` channel, purely to populate breadcrumb/event data.

If the wrapped endpoint later performs its own manual body read against the *same* `receive` callable (rather than through Starlette's cached `Request.body()`/`.stream()`), that second read hangs forever: the ASGI transport already sent its final `http.request` message with `more_body: False`, so there is nothing left to receive, and (on a normal keep-alive connection) no `http.disconnect` will arrive either. The request just hangs until something upstream times out.

This is exactly what happens with Streamlit's `st.file_uploader` (`streamlit/web/server/starlette/starlette_routes.py`, `_upload_put`): to enforce a streaming size cap without buffering the whole upload in memory, it builds a *new* `StarletteRequest(request.scope, limited_receive)` where `limited_receive` calls `await request.receive()` directly. Sentry has already exhausted that channel for any upload under ~10,000 bytes, so the second read blocks indefinitely — every small file upload hangs, every upload over ~10KB (where Sentry skips body capture) works fine. Full writeup and a minimal, Sentry-only repro (isolating exactly which integration is responsible) filed against Streamlit: https://github.com/streamlit/streamlit/issues/16697, gist: https://gist.github.com/tbrambor/d41b40950a0061ca404fc0d67e4768ed

**This isn't Streamlit-specific.** Any ASGI handler that reads its own request body (streaming multipart parsers, chunked uploads, proxies, WebSocket-adjacent code, etc.) rather than going through Starlette's `Request.body()`/`.form()`/`.stream()` cache will break the same way, silently, as a hang rather than an error — for any request whose `Content-Length` happens to fall under `max_request_body_size`. The failure mode (indefinite hang, not an exception) makes it especially hard to trace back to Sentry.

**How do you use Sentry?**

Self-hosted/on-premise, via `sentry-sdk` in a Starlette/FastAPI-family ASGI app.

**Version**

`sentry-sdk==2.61.1`, `starlette==1.3.1` (as bundled with `streamlit==1.61.1`), Python 3.12.

**Steps to Reproduce**

```python
"""
pip install starlette==1.3.1 sentry-sdk==2.61.1 uvicorn
python repro.py
Then: curl -s -X POST http://localhost:8000/upload -F "file=@small.txt" --max-time 5
(any file under 10000 bytes hangs and times out; a file over 10000 bytes succeeds)
"""
import sentry_sdk
sentry_sdk.init(dsn=None)

from starlette.applications import Starlette
from starlette.requests import Request as StarletteRequest
from starlette.responses import PlainTextResponse
from starlette.routing import Route

async def upload(request):
# Mirrors Streamlit's _upload_put: read the body through a fresh
# Request wrapping the same raw ASGI `receive`, instead of the
# cached Request.body()/.form(). This is what breaks once Sentry
# has already drained `receive` for content_length <= 10000 bytes.
limited_request = StarletteRequest(request.scope, request.receive)
form = await limited_request.form()
return PlainTextResponse("ok")

app = Starlette(routes=[Route("/upload", upload, methods=["POST"])])

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
```

1. Run the script above.
2. `curl -X POST http://localhost:8000/upload -F "file=@small.txt"` where `small.txt` is under 10,000 bytes — hangs indefinitely (times out client-side).
3. Same request with a file over 10,000 bytes — succeeds immediately.
4. Comment out `sentry_sdk.init(dsn=None)` — both sizes succeed immediately.

**Expected Result**

The request completes regardless of file size, and regardless of whether the endpoint reads the body itself vs. relying on Sentry having already read it.

**Actual Result**

Requests with a body at or under `max_request_body_size` (default 10,000 bytes) hang forever if the endpoint performs its own body/receive read rather than using Starlette's own cached `Request.body()`/`.form()`. No exception is raised on either side — it's a silent hang, not an error, which makes it very hard to diagnose from application logs alone.

貢獻指南

開啟貢獻指南

研究方向

Start at StarletteIntegration.patch_request_response() and StarletteRequestExtractor.extract_request_info(), then inspect request_body_within_bounds() in _wsgi_common.py to trace when the ASGI receive channel is consumed. Reproduce with the supplied Starlette app and compare requests below and above 10,000 bytes; done means both complete when the endpoint reads the body through a fresh StarletteRequest.

由索引模型根據 Issue 內容生成。

評估

技術堆疊
python
領域
backend-api-design
Issue 類型
缺陷
難度
4/5
預估耗時
3-5 天
活躍度
活躍
描述清晰度
描述清楚
新手友好度
48/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。