MagicStack / MagicStack/httptools
[Bug] Request body lost when Upgrade: h2c + Transfer-Encoding: chunked is used
Nessuno ha ancora preso questa issue.
- Lingua principale
- Python
- Stelle
- 1.3k
- Fork
- 107
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Descrizione
Overview
When sending a POST request from a Java RestClient (Spring Boot 3.2+, Java 21) to a FastAPI backend running on Uvicorn + httptools, we encountered a strange issue where the request body was missing.
The request looked like this:
POST /endpoint HTTP/1.1
Host: my-api.com
Upgrade: h2c
Connection: Upgrade, HTTP2-Settings
Transfer-Encoding: chunked
Content-Type: application/json
3\r\nabc\r\n0\r\n\r\n
On the server side, Uvicorn logs showed:
Unsupported upgrade requestNo request bodyInvalid HTTP request received
But when we routed the same request through ngrok or used RestTemplate instead of RestClient, it worked fine.
🔍 Root Cause
After analyzing Uvicorn’s httptools_impl.py and httptools parser behavior, we found this:
Upgrade: h2cis ignored by Uvicorn (as expected).- But internally,
httptoolsstill enters the upgrade state. - Since the upgrade is ignored and the parser is not reset, no body is parsed.
- This violates RFC 7230 §6.7, which allows the server to ignore upgrades and proceed normally.
Proposed Fix
Patch parser.pyx to resume HTTP/1.1 parsing after upgrade is ignored:
cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1:
cdef HttpParser pyparser = <HttpParser>parser.data
try:
if parser.upgrade and not pyparser._should_upgrade():
cparser.llhttp_resume_after_upgrade(parser)
pyparser._on_headers_complete()
except BaseException as ex:
pyparser._last_error = ex
return -1
return 0
Also expose this from Python:
def resume_after_upgrade(self):
httptools.llhttp_resume_after_upgrade(self.cparser)
Then frameworks like Uvicorn can call it in:
def on_headers_complete(self):
if self.upgrade and self.upgrade.lower() != b"websocket":
self.parser.resume_after_upgrade()
Reproducible Test
def test_chunked_body_with_ignored_upgrade():
headers = {
"Upgrade": "h2c",
"Connection": "Upgrade",
"Transfer-Encoding": "chunked"
}
body = b"4\r\ntest\r\n0\r\n\r\n"
request = b"POST / HTTP/1.1\r\n" + headers_to_bytes(headers) + b"\r\n" + body
parser = HttpRequestParser(TestProtocol())
parser.feed_data(request)
assert protocol.body == b"test"
Why it matters
This is RFC-compliant behavior that should be supported.
RestClient in Java 21+ sends Upgrade: h2c by default.
Any server not resetting its parser state will lose the body.
This breaks many interop scenarios between Spring Boot and Python ASGI apps.
I'm happy to submit a PR if maintainers are open to it. Thanks for your time and for maintaining this great project!
Guida per i contributori
Nessuna guida per i contributori indicizzata per questo repository
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Il callback del parser si trova in parser.pyx; inizia tracciando cb_on_headers_complete e la gestione esistente dell’upgrade, poi esamina il wrapper Python del parser e il test riproducibile del parser. Riproduci la richiesta h2c frammentata e verifica che il body venga preservato dopo un upgrade ignorato, con i test pertinenti del parser superati.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- python
- Ambito
- backend, networking
- Tipo di issue
- Bug
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Stato di attività
- Ferma
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 42/100