From d43acb8fe0910b6669c163e2f4a439e464eab012 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Thu, 26 Mar 2026 15:02:57 +0100 Subject: [PATCH] Update to gunicorn_h1c >= 0.6.2 for asgi_headers support - Use asgi_headers property (lowercase names) from fast parser - Bump version to 25.3.0 - Update changelog with all changes for this release --- docs/content/2026-news.md | 30 ++++++++++++++++++++++++++++++ docs/content/news.md | 30 ++++++++++++++++++++++++++++++ gunicorn/__init__.py | 2 +- gunicorn/asgi/parser.py | 4 +++- pyproject.toml | 6 +++++- requirements_test.txt | 2 +- tests/conftest.py | 6 +++--- 7 files changed, 73 insertions(+), 7 deletions(-) diff --git a/docs/content/2026-news.md b/docs/content/2026-news.md index d4143521..184158a0 100644 --- a/docs/content/2026-news.md +++ b/docs/content/2026-news.md @@ -1,6 +1,36 @@ # Changelog - 2026 +## 25.3.0 - 2026-03-26 + +### Bug Fixes + +- **HTTP/2 ASGI Body Duplication**: Fix request body being received twice in HTTP/2 + ASGI requests, causing JSON parsing errors with "Extra data" messages + ([#3558](https://github.com/benoitc/gunicorn/issues/3558)) + +- **ASGI Chunked EOF Handling**: Add `finish()` method to callback parser to handle + chunked encoding edge case where connection closes before final CRLF after zero-chunk + +### Security + +- **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112: + - Reject duplicate Content-Length headers + - Reject requests with both Content-Length and Transfer-Encoding + - Reject chunked transfer encoding in HTTP/1.0 + - Reject stacked chunked encoding + - Validate Transfer-Encoding values + - Strict chunk size validation + +### Changes + +- **Fast HTTP Parser**: Update to gunicorn_h1c >= 0.6.2 for `asgi_headers` property + which provides headers with lowercase names directly from the C parser + +- **ASGI PROXY Protocol**: Add PROXY protocol v1/v2 support to callback parser + +--- + ## 25.2.0 - 2026-03-24 ### New Features diff --git a/docs/content/news.md b/docs/content/news.md index 5ac5b76e..6be3368d 100644 --- a/docs/content/news.md +++ b/docs/content/news.md @@ -1,6 +1,36 @@ # Changelog +## 25.3.0 - 2026-03-26 + +### Bug Fixes + +- **HTTP/2 ASGI Body Duplication**: Fix request body being received twice in HTTP/2 + ASGI requests, causing JSON parsing errors with "Extra data" messages + ([#3558](https://github.com/benoitc/gunicorn/issues/3558)) + +- **ASGI Chunked EOF Handling**: Add `finish()` method to callback parser to handle + chunked encoding edge case where connection closes before final CRLF after zero-chunk + +### Security + +- **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112: + - Reject duplicate Content-Length headers + - Reject requests with both Content-Length and Transfer-Encoding + - Reject chunked transfer encoding in HTTP/1.0 + - Reject stacked chunked encoding + - Validate Transfer-Encoding values + - Strict chunk size validation + +### Changes + +- **Fast HTTP Parser**: Update to gunicorn_h1c >= 0.6.2 for `asgi_headers` property + which provides headers with lowercase names directly from the C parser + +- **ASGI PROXY Protocol**: Add PROXY protocol v1/v2 support to callback parser + +--- + ## 25.2.0 - 2026-03-24 ### New Features diff --git a/gunicorn/__init__.py b/gunicorn/__init__.py index 26a9e00a..6009fecc 100644 --- a/gunicorn/__init__.py +++ b/gunicorn/__init__.py @@ -2,7 +2,7 @@ # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. -version_info = (25, 2, 0) +version_info = (25, 3, 0) __version__ = ".".join([str(v) for v in version_info]) SERVER = "gunicorn" SERVER_SOFTWARE = "%s/%s" % (SERVER, __version__) diff --git a/gunicorn/asgi/parser.py b/gunicorn/asgi/parser.py index 818a71c6..a1f608a6 100644 --- a/gunicorn/asgi/parser.py +++ b/gunicorn/asgi/parser.py @@ -835,7 +835,9 @@ class CallbackRequest: req.version = parser.http_version # Headers - store both bytes (for ASGI scope) and strings (for compatibility) - req.headers_bytes = list(parser.headers) + # Use asgi_headers (lowercase names) if available (fast parser >= 0.6.2), + # otherwise fall back to headers (Python parser already uses lowercase) + req.headers_bytes = list(getattr(parser, 'asgi_headers', None) or parser.headers) req.headers = [ (n.decode('latin-1').upper(), v.decode('latin-1')) for n, v in parser.headers diff --git a/pyproject.toml b/pyproject.toml index ba579434..07ae3e41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,11 @@ tornado = ["tornado>=6.5.0"] gthread = [] setproctitle = ["setproctitle"] http2 = ["h2>=4.1.0"] -fast = ["gunicorn_h1c>=0.6.0"] + + + +fast = ["gunicorn_h1c>=0.6.2"] + testing = [ "gevent>=24.10.1", "eventlet>=0.40.3", diff --git a/requirements_test.txt b/requirements_test.txt index 3093c4e6..4d26756a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,4 +3,4 @@ coverage pytest>=7.2.0 pytest-cov pytest-asyncio -gunicorn_h1c>=0.6.0 +gunicorn_h1c>=0.6.2 diff --git a/tests/conftest.py b/tests/conftest.py index 018ffbc5..c3824279 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,7 @@ def http_parser(request): """Parametrize tests over http_parser implementations.""" if request.param == "fast": gunicorn_h1c = pytest.importorskip("gunicorn_h1c", reason="gunicorn_h1c required") - # Require >= 0.6.0 for header framing validation - if not hasattr(gunicorn_h1c, 'InvalidHeader'): - pytest.skip("gunicorn_h1c >= 0.6.0 required") + # Require >= 0.6.2 for asgi_headers support + if not hasattr(gunicorn_h1c.H1CProtocol, 'asgi_headers'): + pytest.skip("gunicorn_h1c >= 0.6.2 required") return request.param