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

Aberta
#7,285 3 comentários 0 reações 0 responsáveis Ver no GitHub

Ninguém assumiu esta issue ainda.

Avaliação

Dificuldade
4/5
Tempo estimado
3-5 dias
Facilidade para iniciantes
48/100
Tipo de issue
Bug
Clareza
Claramente especificada
Status de atividade
Ativa
Stack de tecnologia
python

Direção de pesquisa

Comece por StarletteIntegration.patch_request_response() e StarletteRequestExtractor.extract_request_info() e, em seguida, inspecione request_body_within_bounds() em _wsgi_common.py para rastrear quando o canal de recebimento ASGI é consumido. Reproduza com o app Starlette fornecido e compare as requisições abaixo e acima de 10.000 bytes; está concluído quando ambas terminarem ao o endpoint ler o body por meio de uma nova StarletteRequest.

Escrita pelo modelo de indexação a partir do texto da issue.

Descrição

Bug Integration: Starlette/FastAPI Python

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

Linguagem predominante
Python
Estrelas
2.2k
Forks
672
Merge médio
23h 14min
PRs com merge (30d)
218

Guia de contribuição

Abrir o guia de contribuição

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de getsentry/sentry-python

Todas as issues de getsentry/sentry-python

Issues semelhantes

Mais issues de Python

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.