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

Đang mở
#7,285 3 bình luận 0 reaction 0 người được giao Xem trên GitHub
Bug Integration: Starlette/FastAPI Python
Ngôn ngữ chính
Python
Star
2.2k
Fork
669
Merge trung bình
1 ngày 1 giờ
Pull request đã merge (30 ngày)
213

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
backend-api-design
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
48/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.