Reconcile the Frappe-specific gthread changes with upstream's major
26.0.0 release (HTTP/2, ASGI, lock-free main-thread event loop).
Conflict resolution:
- gunicorn/workers/gthread.py: reimplemented our features on top of
upstream's rewritten event loop instead of merging line-by-line:
- Per-request timeout + faulthandler traceback dump, tracked via an
in-flight future set and keyed on exec_start_time so client-wait
time (upstream's _DEFER path) is excluded and long-lived HTTP/2
connections are exempt.
- Adaptive slow/fast-lane queueing: the request-line peek/classify
now reuses upstream's pending_conns poller-park mechanism, then
dispatches to a fast or slow ThreadPoolExecutor.
- Dropped our eventfd-based shutdown wakeup in favour of upstream's
PollableMethodQueue.defer(), which already wakes the poller on
SIGTERM.
- gunicorn/config.py: kept both new validators; our EnableAdaptiveQueueing
and SlowRequestThreshold settings merged cleanly.
- README.rst was removed upstream (converted to README.md); ported an
updated Fork Information section into README.md.
- tox.ini / .github/workflows/tox.yml: took upstream's Python matrix
(it already drops the EoL versions our fork had pruned).
All gthread + routing tests pass (91), full suite 1976 passed / 263
skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the ASGI strip-and-warn behavior (commits 2191832b, 41ec7527,
0d35d2ae) on the WSGI path. Previously gunicorn would forward an
app-supplied Content-Length and body bytes for HEAD requests and
1xx/204/304 responses, violating RFC 9110 / RFC 9112.
- Add _response_omits_body() and _response_forbids_content_length()
helpers on Response.
- After process_headers, strip Content-Length and clear
response_length on 1xx/204 (RFC 9110 §6.4.2 forbids it). HEAD and
304 keep app-supplied Content-Length.
- write() and sendfile() drop body bytes for no-body responses and
log a single WARNING per request.
- is_chunked() now also covers 1xx via _omits_body.
Fixes#3413
Eventlet was deprecated for 26.0 and is now removed:
- Delete gunicorn/workers/geventlet.py and its registry entry
- Drop eventlet from config help text, HTTP/2 unsupported-worker
messages, and the dirty client docstring
- Drop the eventlet optional-dependency, the eventlet entry in the
testing extra, and the eventlet-only filterwarnings ignore
- Drop the EventletWorkerAlpn test class
- Drop the freebsd CI ignore for the (now non-existent) test_geventlet.py
- Drop eventlet from the issue-triage discussion template
- Drop eventlet from README, install/design/http2/settings/news docs;
rewrite the news.md entry from 'deprecated' to 'removed in this release'
Add h2 and uvloop to requirements_test.txt so a plain
'pip install -r requirements_test.txt' run reaches feature parity with
'pip install .[testing]' for those two deps. The container suite
previously skipped 87 HTTP/2 tests for missing h2 and 1 for uvloop;
the in-process suite skips drop from 67 to 40.
- /unlimited and /limited handlers passed the data dict where the dirty
client expected the action (method) name, surfacing as a 500 from
getattr(self, action) on the dirty worker. Pass 'process' as the
action so the call routes to DirtyApp.process(data).
- TestUnlimitedApps now bumps worker count via TTIN and polls both apps
for readiness before each test. The preceding TTOU-spam test pins the
worker count at the LimitedTask floor (2) and the arbiter takes a
moment to rebind apps to the surviving workers; the previous tests
raced that rebind and saw 'No workers available'.
- per_app_allocation: move host port from 8001 to 28001. OrbStack reserves
8001 on macOS for vcom-tunnel which makes 'Bind: port already allocated'
the default failure mode.
- dirty_ttin_ttou: pin BASE_URL to 127.0.0.1 instead of 'localhost'. macOS
resolves 'localhost' to ::1 first; Docker Desktop / OrbStack only forward
host ports on IPv4 so the IPv6 attempt resets and the test fixture treats
the service as unhealthy.
- dirty_ttin_ttou: add setproctitle to the test image. The TTIN/TTOU tests
count workers via 'pgrep -f dirty-worker', which only matches once
gunicorn's util._setproctitle has actually renamed the processes.
RFC 9110 §6.4.2 forbids Content-Length only on 1xx and 204 responses.
HEAD MAY include the Content-Length the same GET would return, and 304
MAY include the Content-Length the unconditional response would carry.
WSGI preserves app-supplied Content-Length on those statuses; ASGI was
stripping it indiscriminately for any no-body response.
Split the predicate: _response_forbids_content_length() returns True
only for 1xx/204; _strip_body_framing_headers(headers, status) always
strips Transfer-Encoding (no body, no chunked terminator) and strips
Content-Length only when forbidden.
Three findings against the ASGI PROXY protocol path:
- High: an untrusted peer could send a PROXY v1/v2 header and have the
client address surfaced to the app. _setup_callback_parser now passes
proxy_protocol='off' to the parser when the peer is not in
proxy_allow_ips. _effective_peername adds a defensive re-check.
- Medium: PROXY v1 TCP4/TCP6 addresses were copied as strings without
validation. Validate with socket.inet_pton, mirroring the WSGI parser.
- Medium: PROXY v2 quietly mapped non-STREAM (DGRAM) protocols to
UDP4/UDP6. gunicorn is an HTTP server; reject non-STREAM with
InvalidProxyHeader, mirroring the WSGI parser.
The previous test forced http_parser='python' to avoid a hard
dependency on gunicorn_h1c. Now run the same scenario under both
parser implementations so the smuggling guard is exercised on every
supported request-line/header path.
The smuggling guard added in #3614 reads self._body_receiver after
_handle_http_request returns to refuse keepalive on a body that did
not finish framing. But _handle_http_request's finally cleared the
receiver before returning, so the gate always saw None and let
keepalive proceed unconditionally. Move the clear into the
connection loop's per-iteration cleanup (it already had one there
for the same purpose).
Adds an end-to-end regression test that pipelines a partial-body POST
followed by a smuggled GET and asserts the second request is not
served and the transport closes.
_closed now means the client transport has gone away. Body-wait timeouts
flip a separate _body_wait_expired flag. Both still surface as
http.disconnect to the app, but downstream code can now distinguish 'the
socket is dead' from 'the body never finished framing in time' without
guessing which path set the flag.
RFC 9110 forbids a body for HEAD requests and for 1xx/204/304 status
codes. PR #3614 stopped gunicorn from auto-applying chunked encoding
in those cases, but if the application explicitly emitted a
Content-Length or Transfer-Encoding header (and possibly body bytes),
gunicorn still passed them through. Now strip both headers, force
plain framing, and discard any body the app emits.
The previous assertion ran immediately after a 2s sleep and raced
the arbiter's socket re-creation on slow runners (observed flake on
FreeBSD 14.2 / Python 3.13). Replace with the wait_for_socket helper
already used elsewhere in the file.
- ASGI keepalive gate now keys on receiver._complete only. _closed is
overloaded across transport disconnect and receive timeout; treating
either as 'message complete' would re-enable the smuggling vector
the previous PR was meant to close.
- Parser.finish_body's 64 KiB byte cap now applies only when an explicit
deadline is given. Default invocations (notably __next__, used by
base_async / sync workers) regain the prior unbounded drain so a
partial drain does not silently desync the next request.
- WSGI fast parser now applies the same per-header policy as the Python
parser (Expect, secure_scheme_headers, forwarded_allow_ips trust gate,
forwarder_headers / header_map). Shared helpers extracted on Message.
- ASGI keepalive no longer resets the parser when the previous request
body was not fully framed; the connection closes instead, preventing
request smuggling on pipelined connections.
- BodyReceiver._wait_for_data timeout flips _closed and yields
http.disconnect rather than synthesizing more_body=False. Timeout
honors cfg.timeout.
- ASGI chunked encoding now skips HEAD, 204, and 304 (matches
Response.is_chunked in the WSGI path) via a small helper.
- _setup_callback_parser passes proxy_protocol to PythonProtocol; auto
falls back to the Python parser when proxy_protocol != off (the C
parser does not implement PROXY framing). _effective_peername swaps
the transport peer with the PROXY-supplied client address.
- Parser.finish_body accepts a deadline and a 64KiB byte cap; gthread
passes a deadline and abandons keepalive on incomplete drain so a
stalled client cannot tie up a worker thread.
gunicorn_h1c 0.6.5 ships the Content-Length list-form rejection
(h1c #8). The last python_only marker can now come off
rfc9112_smuggle_cl_list_form_01.
gunicorn_h1c 0.6.4 ships the RFC 9110/9112 hardening added in h1c #4,
#6, and #7: control chars in header values, request-target form/method
pairing, and forbidden trailer field-names. All the corresponding
fixtures now pass against the C parser, so their python_only markers
are removed.
The CL list form fixture stays marked — the C parser does not yet
reject Content-Length: "5, 5".
The Python WSGI and ASGI parsers both accepted `GET *` and similar; RFC
9112 restricts asterisk-form to OPTIONS. Both now raise InvalidRequestLine.
The fast (C) parser in gunicorn_h1c does not yet enforce this, so the
fixture is marked python_only via a new sidecar flag honored by the WSGI
and ASGI invalid-request harnesses.
Six treq fixtures covering gaps: absolute-form, asterisk-form (OPTIONS *),
authority-form (CONNECT), TE codings stacking (gzip/identity before chunked),
and the CL + TE:chunked smuggling vector.
Phase 1 of a staged corpus expansion; fixtures only, no parser changes.
The early_hints callback constructs 103 Early Hints responses without
any header validation, while process_headers validates against TOKEN_RE
and HEADER_VALUE_RE for normal responses. This inconsistency means a
WSGI app passing unsanitized data to wsgi.early_hints could enable
HTTP response splitting via CRLF injection.
Apply the same TOKEN_RE/HEADER_VALUE_RE checks from process_headers to
the early_hints callback for defense-in-depth consistency.
Closes#3585
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