Litestar internally caches request.body() and request.headers which
caused stale data to be returned on subsequent requests over keep-alive
connections. Access body via receive callable and headers directly from
scope to avoid this caching behavior.
This allows testing local changes to gunicorn in the E2E test suite.
Previously containers were installing from GitHub master branch.
Also updates compatibility grid with latest test results (417/444, 93%).
When frameworks like BlackSheep set Transfer-Encoding: chunked on
streaming responses, gunicorn was adding a second header without
checking if one already exists. This caused httpcore to reject the
response with "multiple Transfer-Encoding headers" error.
Fix checks for existing Transfer-Encoding header before adding one,
while still enabling chunked body encoding when the framework sets it.
- Add _close_sent, _close_received, _close_event state variables
- Server now waits for client's close frame response before marking
connection as closed (5s timeout)
- Update _read_frames loop to continue reading after sending close
- Fix tests to simulate client close frame response
BodyReceiver.receive() now blocks after body is finished until actual
disconnect, instead of returning http.disconnect immediately. This fixes
Django's listen_for_disconnect task thinking client disconnected early.
Adds regression tests for the fix.
Fixes#3484
_handle_stream_ended() in async_connection.py (used by the ASGI worker)
did not set stream._body_complete = True or signal stream._body_event.
This caused the receive() closure in protocol.py to never see the body
as complete via the streaming path, so on the next call the fast path
re-read the entire body from BytesIO, doubling it.
The sync handler in connection.py already had a partial fix from #3559
but was also missing _body_event signalling, which is needed to unblock
any pending read_body_chunk() await.
Fixes https://github.com/benoitc/gunicorn/discussions/3567
* Add InvalidChunkExtension to treq_asgi.py and fast parser support
- Add InvalidChunkExtension import and exception mapping for proper test
coverage of bare CR rejection in chunk extensions per RFC 9112 7.1.1
- Add fast parser (H1CProtocol) support to treq_asgi.py and the ASGI
invalid request tests
- Fast parser now receives limit configuration (limit_request_line,
limit_request_fields, limit_request_field_size)
- Handle gunicorn_h1c's multiple ParseError classes from different modules
- Skip tests where fast parser has different validation than Python parser
* Handle gunicorn_h1c limit exceptions in ASGI protocol
Add handling for gunicorn_h1c.LimitRequestLine and
gunicorn_h1c.LimitRequestHeaders exceptions, matching the behavior
of the Python parser exceptions with appropriate HTTP status codes:
- LimitRequestLine: 414 URI Too Long
- LimitRequestHeaders: 431 Request Header Fields Too Large
* Refactor data_received to fix too-many-return-statements lint
Per documentation, limit_request_line=0 means unlimited. The code was
incorrectly treating 0 as "use default max" by checking <= 0 instead
of < 0.
For the fast C parser (gunicorn_h1c), which doesn't support 0 as
unlimited, pass a large value (1MB) instead. This applies to both
WSGI workers (http/message.py) and ASGI workers (asgi/protocol.py).
Fixes#3563
AsyncRequest was the legacy pull-based async HTTP parser, now replaced
by the push-based CallbackRequest/PythonProtocol. Remove the unused
code and associated tests.
Update to gunicorn_h1c >= 0.6.3 which adds InvalidChunkExtension
validation for rejecting chunk extensions with bare CR bytes per
RFC 9112.
Changes:
- Update pyproject.toml to require gunicorn_h1c >= 0.6.3
- Add InvalidChunkExtension exception to gunicorn/asgi/parser.py
- Handle InvalidChunkExtension from both Python and C parsers in protocol.py
- Add chunk extension validation tests
- Update treq.py badrequest class to support hex escapes
Handle chunked encoding edge case where connection closes before
final CRLF after zero-chunk. Skip WSGI-specific tests (casefold,
underscore headers) that don't apply to ASGI.
receive_data() stores every DATA frame in both _body_chunks (list)
and request_body (BytesIO). The receive() closure in
_handle_http2_request() has two read paths: a streaming path that
pops from _body_chunks, and a fast path that reads from BytesIO.
After the streaming path consumed the body, the fast path could
re-read the same data from BytesIO because body_received was never
set in the streaming return path. This caused the application to
receive a doubled request body (e.g. 18 bytes sent, 36 bytes
received), breaking JSON parsing with "Extra data" errors.
Fix: set body_received = True in the streaming path when
_body_complete is True, preventing the fast path from re-reading
already-consumed data.
Fixes#3558