[Bug] Request body lost when Upgrade: h2c + Transfer-Encoding: chunked is used

オープン
#124 コメント 0 件 リアクション 2 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
42/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
停滞
技術スタック
python

調査の方向性

パーサーのコールバックは parser.pyx にあります。まず cb_on_headers_complete と既存の upgrade 処理を追跡し、次に Python パーサーラッパーと再現可能なパーサーテストを調べてください。チャンク化された h2c リクエストを再現し、無視された upgrade の後も body が保持されることを確認するとともに、関連するパーサーテストが通ることを確認してください。

索引モデルが issue の本文から書いたものです。

説明

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 request
  • No request body
  • Invalid 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: h2c is ignored by Uvicorn (as expected).
  • But internally, httptools still 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!

主要言語
Python
スター
1.3k
フォーク
107
PR マージ指標
30日以内にマージされた PR はありません

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

MagicStack/httptools のほかの issue

MagicStack/httptools の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。