StarletteIntegration eagerly drains the ASGI receive channel for request bodies under max_request_body_size, breaking handlers that read the body themselves afterward
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 48/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- python
- Área
- backend-api-design
Línea de trabajo
Empieza por StarletteIntegration.patch_request_response() y StarletteRequestExtractor.extract_request_info(), y después inspecciona request_body_within_bounds() en _wsgi_common.py para rastrear cuándo se consume el canal de recepción de ASGI. Reprodúcelo con la aplicación de Starlette proporcionada y compara las solicitudes inferiores y superiores a 10.000 bytes; se considera completado cuando ambas terminan al leer el endpoint el body mediante una nueva StarletteRequest.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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
"""
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)
- Run the script above.
curl -X POST http://localhost:8000/upload -F "file=@small.txt"wheresmall.txtis under 10,000 bytes — hangs indefinitely (times out client-side).- Same request with a file over 10,000 bytes — succeeds immediately.
- 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.
- Lenguaje dominante
- Python
- Estrellas
- 2.2k
- Forks
- 672
- Merge medio
- 22 h 47 min
- PR fusionados (30 d)
- 224
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de getsentry/sentry-python
-
Python
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
getsentry/sentry-python#7569 · 1 comentario ·
-
Python
Dificultad 1/5 Menos de una hora Aptitud para principiantes 85/100
getsentry/sentry-python#7568 · 2 comentarios ·
-
Python
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
getsentry/sentry-python#7567 · 1 comentario ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
getsentry/sentry-python#7543 · 2 comentarios · 1 asignado ·
-
Python
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
getsentry/sentry-python#6992 · 1 comentario ·
Todos los issues de getsentry/sentry-python
Issues similares
-
enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
canonical/paas-charm#368 · 1 comentario ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
-
tech debt
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
-
addition to tracking list Abierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 90/100
StevenBlack/hosts#3256 ·
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 90/100
qualcomm/qai-appbuilder#275 ·