Skip to content

fix: add header validation to early_hints callback - #3588

Merged
benoitc merged 2 commits into
benoitc:masterfrom
eddieran:fix/early-hints-header-validation
Apr 19, 2026
Merged

benoitc merged 2 commits into
benoitc:masterfrom
eddieran:fix/early-hints-header-validation

Conversation

@eddieran

Copy link
Copy Markdown
Contributor

Summary

Fixes #3585 — the wsgi.early_hints callback constructs 103 Early Hints responses with zero header validation, while Response.process_headers already validates header names against TOKEN_RE and header values against HEADER_VALUE_RE. This gap means a WSGI app passing unsanitized user input to wsgi.early_hints could enable HTTP response splitting via CRLF injection.

What this does

Adds the same TOKEN_RE.fullmatch(name) / HEADER_VALUE_RE.fullmatch(value) checks (and the value.strip(" \t") normalization) from process_headers into send_early_hints. Raises the same InvalidHeaderName / InvalidHeader exceptions on failure.

Addressing maintainer concerns

Frameworks should do it. Maybe we should ensure to not do the work twice.

Agreed — frameworks should validate. But gunicorn already validates in process_headers for normal responses, so this PR simply brings early_hints to parity. It's defense-in-depth: if the framework misses it, the server catches it. The regex checks are fast and only run on the (typically small) early hints header list, so the cost is negligible.

Test plan

  • Added tests for CRLF injection in header values → InvalidHeader raised, nothing written to socket
  • Added tests for CRLF injection in header names → InvalidHeaderName raised
  • Added tests for invalid token characters in header names → InvalidHeaderName raised
  • Verified valid headers still pass through correctly
  • All 11 existing + new early hints tests pass

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 benoitc#3585
Comment thread gunicorn/http/wsgi.py Outdated
if not TOKEN_RE.fullmatch(name):
raise InvalidHeaderName('%r' % name)
if not HEADER_VALUE_RE.fullmatch(value):
raise InvalidHeader('%r' % value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until error handling receives some major cleanup (preferably class inheritance instead of massive elif) that exception class should be instantiated with just the name, not the value. Otherwise any way of triggering the exception on purpose can be used move sensitive information between security boundaries. Browsers are picky about what headers can be accessed cross-site, and proxies sitting between Gunicorn and the client might pick which ones to forward. Best not to interfere with that.

Per @pajod review: the invalid header value may carry sensitive
content, and raising it through the exception could leak it
across security boundaries (browsers/proxies handling response
splitting errors). Pass just the name instead.
@eddieran

Copy link
Copy Markdown
Contributor Author

Addressed, @pajod — thanks for catching the cross-boundary leak. Changed raise InvalidHeader('%r' % value)raise InvalidHeader('%r' % name) so the value never appears in the exception. Agreed that the existing process_headers on line 350 has the same pattern, but since you flagged that it's pending the broader error-handling cleanup, I left it alone in this PR.

@benoitc
benoitc merged commit e5c30b4 into benoitc:master Apr 19, 2026
luketainton pushed a commit to luketainton/luke_rsu that referenced this pull request Sep 1, 2026
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [gunicorn](https://github.com/benoitc/gunicorn) ([changelog](https://gunicorn.org/news/)) | `>=23,<24` → `>=26,<27` | ![age](https://developer.mend.io/api/mc/badges/age/pypi/gunicorn/26.2.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/gunicorn/23.0.0/26.2.0?slim=true) |

---

> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/11) for more information.

---

### Release Notes

<details>
<summary>benoitc/gunicorn (gunicorn)</summary>

### [`v26.2.0`](https://github.com/benoitc/gunicorn/releases/tag/26.2.0): gunicorn 26.2.0

[Compare Source](benoitc/gunicorn@26.1.0...26.2.0)

Cleartext HTTP/2 lands, and an HTTP/2 security fix.

#### Cleartext HTTP/2 (h2c)

`http2_cleartext` accepts `prior-knowledge`, `upgrade`, `both` or `off` (the
default). Prior knowledge serves a connection that opens with the HTTP/2
preface; `upgrade` honours an HTTP/1.1 `Upgrade: h2c` request. Both work on the
gthread, gevent and asgi workers.

This is for deployments where TLS is terminated by a proxy that speaks HTTP/2
upstream, so the hop into gunicorn no longer drops to HTTP/1.1. Only peers in
`forwarded_allow_ips` are considered; everyone else is served HTTP/1.x exactly
as if the setting were off. Each mechanism is enabled separately, so turning one
on does not turn the other on.

Do not expose a cleartext HTTP/2 port to the internet.

#### Security

`HTTP2Request` built its headers straight from the stream, so nothing the HTTP/1
path enforces applied over HTTP/2: the underscore and `header_map` policy,
duplicate `Host` and `Content-Type`, control characters in values, and the
`forwarded_allow_ips` trust gate. An untrusted client could set `SCRIPT_NAME`
and forge `HTTP_*` entries in the WSGI environ, and decide `wsgi.url_scheme`
through `:scheme`. Both request classes now share one policy mixin, and the
scheme comes from the transport.

If you serve HTTP/2, this is the reason to upgrade.

#### Other HTTP/2 fixes

WSGI responses were buffered whole before anything was sent; they stream now.
HEAD, 204 and 304 no longer carry a body. Events read while blocked on a
flow-control window were discarded, losing requests and body data outright.
`sendfile()` is refused on HTTP/2 responses rather than bypassing framing.

#### Request bodies dropped on Upgrade requests

On the ASGI worker with the fast parser, any request carrying an `Upgrade`
header reached the application with an empty body, whatever the header's value
and with HTTP/2 switched off entirely. Fixed in `gunicorn_h1c` 0.6.9, which the
`fast` extra now requires.

Full changelog: <https://gunicorn.org/news/>

### [`v26.1.0`](https://github.com/benoitc/gunicorn/releases/tag/26.1.0): gunicorn 26.1.0

[Compare Source](benoitc/gunicorn@26.0.0...26.1.0)

##### New Features

- **Glob patterns in `reload_extra_files`**: entries containing `*`, `?` or `[`
  are treated as patterns, so `ui/*/config.json` watches every view's config
  without listing them one by one. Patterns are re-expanded on every reload
  check rather than once at startup, so a file created later starts being
  watched without restarting gunicorn, and `**` recurses. A pattern matching
  nothing warns instead of failing, since with live expansion it may match later
  ([#&#8203;1643](benoitc/gunicorn#1643),
  [#&#8203;3662](benoitc/gunicorn#3662)).

##### Security

- **Dependency floors raised past known advisories**: every declared floor was
  checked against the advisory database. `tornado`, `h2`, `setuptools` and
  `pymdown-extensions` permitted vulnerable versions and now require the first
  clean release; `pytest` and `httpx` were unpinned and now carry floors. The
  `tornado` example pinned `tornado<6`, which was both the source of several
  advisories and older than the `>=6.5.0` the tornado worker needs, so the
  example could not run as pinned.

##### Bug Fixes

- **SIGHUP did not reload the logger configuration**: `Arbiter.reload()`
  re-read the configuration file but kept using the logger built at startup,
  calling only `reopen_files()` on its existing handlers. Changes to
  `logconfig`, `logconfig_dict`, `logconfig_json` and `loglevel` were ignored
  until a full restart, which in containers meant replacing the pod. The
  existing logger now re-runs its setup on reload, so new handlers, formats
  and levels take effect while the process identity and its listeners are
  preserved, and re-running the setup no longer stacks duplicate syslog
  handlers. An invalid log configuration on reload is not fatal either: the
  error is reported on stderr, the previous working configuration is restored
  and the master keeps running with it
  ([#&#8203;3353](benoitc/gunicorn#3353)).

- **Truncated chunked bodies accepted**: RFC 9112 section 7.1.2 ends a chunked
  body with `0 CRLF CRLF`, the second CRLF being the mandatory empty trailer
  section. `ChunkedReader.parse_chunk_size()` swallowed the `NoMoreData` raised
  while scanning for it, so a body cut short right after the last chunk line was
  treated as complete instead of rejected. It now raises
  `ChunkMissingTerminator`
  ([#&#8203;3382](benoitc/gunicorn#3382),
  [#&#8203;3685](benoitc/gunicorn#3685)).

- **`--spew` crashed on dynamically generated code**: the trace hook indexed the
  2-tuple returned by `inspect.getsourcelines()` by line number rather than
  indexing the list of lines, so a frame with no `__file__` raised
  `AttributeError: 'int' object has no attribute 'rstrip'` on line 1 and
  `IndexError` beyond it. The tuple is now unpacked and offset by the source's
  starting line ([#&#8203;3344](benoitc/gunicorn#3344),
  [#&#8203;3495](benoitc/gunicorn#3495)).

- **Duplicate `Host` and `Content-Type` headers accepted**: RFC 9110 section 5.3
  allows only one of each, and a repeat cannot be merged into a list, so the
  message means different things to gunicorn and to anything downstream. Both
  are now rejected with `InvalidHeader`. The check lives in the policy hook
  shared by both parsers, so the pure-Python and fast parsers agree. Duplicate
  `Content-Length` was already rejected and is unchanged
  ([#&#8203;3366](benoitc/gunicorn#3366),
  [#&#8203;3548](benoitc/gunicorn#3548)).

- **Non-worker children reported as failed workers**: `reap_workers()` reaps
  every child through `waitpid(-1)`, including processes the kernel reparented
  onto gunicorn when it runs as PID 1 in a container, but it logged the exit
  status before checking whether the pid was ever a worker. An unrelated process
  produced `Worker (pid:N) exited with code M` and triggered alerts. More
  seriously, such a process exiting with code 3 or 4 raised `HaltServer` and shut
  the server down. Ownership is now established first: the dirty arbiter is
  reported as itself, unknown children are reaped silently at debug level, and
  only real workers can halt the server
  ([#&#8203;3220](benoitc/gunicorn#3220),
  [#&#8203;3566](benoitc/gunicorn#3566)).

- **Dirty arbiter exits were invisible on SIGCHLD**: `handle_chld()` called
  `reap_workers()` first, whose `waitpid(-1)` claimed the dirty arbiter before
  `reap_dirty_arbiter()` could identify it, so the latter always hit `ECHILD` and
  its reporting never ran. The dirty arbiter is now reaped first, and
  `reap_workers()` recognises it if it exits mid-loop.

- **Dirty arbiter returned stale responses after a worker timeout**: when a
  request reached `dirty_timeout` the arbiter answered the client with a timeout
  error but kept the worker connection open. The worker's late response was then
  the first message waiting on that socket, so the next request routed to the
  same worker received the previous request's result, and every request after it
  stayed one response behind. The connection is now closed on timeout, so the
  late answer is discarded with it
  ([#&#8203;3626](benoitc/gunicorn#3626)).

- **ASGI connection count leaked on server-initiated close**: `nr_conns` was
  only decremented in `connection_lost()`, behind a guard keyed on the same
  flag `_close_transport()` sets first. Every close the server started (a
  `Connection: close` response, a keepalive timeout, an error abort) leaked one
  count, so `ASGIWorker._shutdown()` ran the full `graceful_timeout` and warned
  about connections that were already gone. The guard now uses its own flag, so
  the decrement and the rest of the cleanup run exactly once whichever side
  closes first ([#&#8203;3661](benoitc/gunicorn#3661)).

- **Inotify reloader on cwd-relative extra files**: `reload_extra_files` entries
  with no directory part (for example `.env`) produced an empty dirname, and
  watching it raised `InotifyError` with `ENOENT`. The current directory is now
  watched as `.` ([#&#8203;3377](benoitc/gunicorn#3377),
  [#&#8203;3667](benoitc/gunicorn#3667)).

- **StatsD zero-valued metrics**: gauges, counters, histograms and timers
  reporting `0` were silently dropped because the value was tested for
  truthiness. Only `None` is skipped now
  ([#&#8203;3676](benoitc/gunicorn#3676)).

- **Spurious no-body warning from `sendfile()`**: a HEAD, 204 or 304 response
  served through `sendfile()` warned about dropped body bytes even when the
  file was empty and nothing was dropped. It now warns only when there are
  bytes to drop, matching `write()`
  ([#&#8203;3684](benoitc/gunicorn#3684)).

- **Bare `except` in the gevent websocket example**: narrowed to
  `except Exception` ([#&#8203;3683](benoitc/gunicorn#3683)).

- **ASGI `receive()` cancellation**: Let `asyncio.CancelledError` propagate
  from `BodyReceiver` instead of swallowing it and returning
  `http.disconnect`. Frameworks that cancel their disconnect listener after
  the response completes (Django) no longer see the cancel masked, so
  `request_finished` fires and `close_old_connections()` runs. Fixes idle
  database connections leaking since 25.1.0
  ([#&#8203;3627](benoitc/gunicorn#3627),
  [#&#8203;3654](benoitc/gunicorn#3654)).

- **Control socket leak on SIGHUP reload**: The control thread is now marked
  ready once its loop and server are live, and the stop paths wait on that
  readiness before scheduling shutdown. Reloads no longer leak one thread and
  its selector fd plus unix socket per worker, which eventually raised
  "too many open files"
  ([#&#8203;3648](benoitc/gunicorn#3648)).

- **WSGI body framing on HEAD/1xx/204/304**: Mirror the ASGI strip-and-warn
  behavior on the WSGI path. `Content-Length` is stripped on 1xx/204 per
  RFC 9110 section 6.4.2, body bytes are dropped for no-body responses in
  both `write()` and `sendfile()`, and a single warning is logged per request
  ([#&#8203;3413](benoitc/gunicorn#3413)).

##### Refactoring

- Pass log arguments to the logger instead of pre-formatting the worker
  termination message in `Arbiter.reap_workers()`
  ([#&#8203;3678](benoitc/gunicorn#3678)).

##### Changes

- **`packaging` is no longer a runtime dependency**: it was only ever imported by
  the gevent worker, to compare gevent's version. It moved to the `gevent` and
  `testing` extras, so a plain `pip install gunicorn` pulls in nothing
  ([#&#8203;3643](benoitc/gunicorn#3643)).

- **Fast HTTP Parser**: Require `gunicorn_h1c >= 0.6.6`, which rejects duplicate
  `Host` and `Content-Type` headers in the C parser itself. Gunicorn already
  refuses them on both the WSGI and ASGI paths, so this changes nothing that is
  reachable; it moves the rejection to where the bytes are read and lets the
  ASGI corpus exercise those cases against the fast parser directly.

Full changelog: <https://gunicorn.org/2026-news/>

### [`v26.0.0`](https://github.com/benoitc/gunicorn/releases/tag/26.0.0)

[Compare Source](benoitc/gunicorn@25.3.0...26.0.0)

#### Breaking Changes

- **Eventlet worker removed**: The `eventlet` worker class has been dropped. Migrate to `gevent`, `gthread`, or `tornado`.

#### New Features

- **ASGI Framework Compatibility Suite**: New end-to-end compatibility test harness covering Starlette, FastAPI, Litestar, Quart, Sanic, and BlackSheep. Current grid passes 438/444 tests (98%).
- **ASGI Test Suite Expansion**: 134 additional ASGI unit tests covering protocol semantics, lifespan, websockets, and chunked framing.

#### Security

- **HTTP/1.1 Request-Target Validation** (RFC 9112 sections 3.2.3, 3.2.4):
  - Reject `authority-form` request-target outside `CONNECT`
  - Reject `asterisk-form` request-target outside `OPTIONS`
  - Reject `relative-reference` request-targets
- **Header Field Hardening** (RFC 9110):
  - Reject control characters in header field-value (section 5.5)
  - Reject forbidden trailer field-names (section 6.5.1)
  - Reject `Content-Length` list form (RFC 9112 section 6.3)
- **Request Smuggling Hardening**:
  - Tighten keepalive gate and scope `finish_body` byte cap
  - Keep `_body_receiver` alive across the keepalive smuggling gate so pipelined requests cannot re-enter a closed body
  - Address parser/protocol findings from a six-point WSGI/ASGI audit
- **PROXY Protocol (ASGI)**: Enforce `proxy_allow_ips` and tighten v1/v2 parsing in the ASGI callback parser.
- **Connection Draining**: Drain the connection on close per RFC 9112 section 9.6 to prevent reset-on-close truncation.

#### Bug Fixes

- **Body Framing on HEAD/204/304**:
  - Keep `Content-Length` on HEAD and 304 responses ([#&#8203;3621](benoitc/gunicorn#3621))
  - Drop body framing on HEAD/204/304 even when the framework set it
  - Warn once when an ASGI app emits a body for a no-body response
- **HTTP/2 ASGI**:
  - Fix `_handle_stream_ended` to set `_body_complete` in the async HTTP/2 handler so request bodies finalize correctly on stream end
  - Add `InvalidChunkExtension` mapping and fast-parser support in ASGI tests ([#&#8203;3565](benoitc/gunicorn#3565))
- **HTTP/1.1 100-Continue**: Stop adding `Transfer-Encoding: chunked` to 100-Continue interim responses.
- **WebSocket Close Handshake** (RFC 6455):
  - Comply with the close handshake state machine
  - Close the transport after the close handshake completes
  - Fix binary send when the `text` key is `None`
- **Early Hints**: Validate headers in the `early_hints` callback to match `process_headers`; pass only the header name to `InvalidHeader` ([#&#8203;3588](benoitc/gunicorn#3588)).
- **ASGI Framework Fixes**:
  - Fix ASGI disconnect handling for Django-style apps
  - Fix Litestar request handling (use raw ASGI receive for body/headers)
  - Fix Litestar HTTP endpoints for compatibility tests
  - Fix Quart headers endpoint to normalize keys to lowercase
  - Fix Quart WebSocket close test app (missing `accept()`)
  - Fix duplicate `Transfer-Encoding` header for BlackSheep streaming

#### Refactoring

- Split `BodyReceiver._closed` into separate transport and body-wait flags for clearer keepalive/EOF semantics.

#### Changes

- **Fast HTTP Parser**: Require `gunicorn_h1c >= 0.6.5`. Drop the last `python_only` test markers; the C extension is now used wherever available (CPython only; PyPy continues to use the Python parser).
- **Test Dependencies**: Add `h2` and `uvloop` to the `testing` extra; remove `eventlet`.
- **Docker Build**: Bump GitHub Actions `docker/setup-qemu-action`, `docker/setup-buildx-action`, `docker/login-action`, `docker/build-push-action`, and `docker/metadata-action` to current major versions.

**Full changelog**: <benoitc/gunicorn@25.3.0...26.0.0>

### [`v25.3.0`](https://github.com/benoitc/gunicorn/releases/tag/25.3.0): Gunicorn 25.3.0

[Compare Source](benoitc/gunicorn@25.2.0...25.3.0)

#### 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
  ([#&#8203;3558](benoitc/gunicorn#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

- **HTTP/2 Documentation**: Fix `http_protocols` examples to use comma-separated string
  instead of list syntax ([#&#8203;3561](benoitc/gunicorn#3561))

- **Chunked Encoding**: Reject chunk extensions containing bare CR bytes per RFC 9112
  ([#&#8203;3556](benoitc/gunicorn#3556))

- **Request Line Limit**: Fix `--limit-request-line 0` to mean unlimited as documented,
  instead of using default maximum. Works with both Python and fast C parser.
  ([#&#8203;3563](benoitc/gunicorn#3563))

#### 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.3 for `asgi_headers` property
  and `InvalidChunkExtension` validation for bare CR rejection

- **ASGI PROXY Protocol**: Add PROXY protocol v1/v2 support to callback parser

- **Docker Images**: Update to Python 3.14

### [`v25.2.0`](https://github.com/benoitc/gunicorn/releases/tag/25.2.0): Gunicorn 25.2.0

[Compare Source](benoitc/gunicorn@25.1.0...25.2.0)

##### New Features

- **Fast HTTP Parser (gunicorn\_h1c 0.4.1)**: Integrate new exception types and limit parameters from gunicorn\_h1c 0.4.1 for both WSGI and ASGI workers
  - Requires gunicorn\_h1c >= 0.4.1 for `http_parser='fast'`
  - Falls back to Python parser in `auto` mode if version not met
  - Proper HTTP status codes for limit errors (414, 431)

##### Bug Fixes

- **uWSGI Async Workers**: Fix `InvalidUWSGIHeader: incomplete header` error when using gevent or gthread workers with uwsgi protocol behind nginx. ([#&#8203;3552](benoitc/gunicorn#3552), [PR #&#8203;3554](benoitc/gunicorn#3554))

- **FileWrapper Iterator Protocol**: Add `__iter__` and `__next__` methods to `FileWrapper` for full PEP 3333 compliance. ([#&#8203;3396](benoitc/gunicorn#3396), [PR #&#8203;3550](benoitc/gunicorn#3550))

##### Performance

- **ASGI HTTP Parser Optimizations**: Improve ASGI worker HTTP parsing performance
  - Callback-based parsing with direct `bytearray` buffer operations
  - Use `bytearray.find()` directly instead of converting to bytes first
  - Use index-based iteration for header parsing instead of `list.pop(0)` (O(1) vs O(n))

### [`v25.1.0`](https://github.com/benoitc/gunicorn/releases/tag/25.1.0): Gunicorn 25.1.0

[Compare Source](benoitc/gunicorn@25.0.3...25.1.0)

##### New Features

- **Control Interface (gunicornc)**: Add interactive control interface for managing
  running Gunicorn instances, similar to birdc for BIRD routing daemon
  ([PR #&#8203;3505](benoitc/gunicorn#3505))
  - Unix socket-based communication with JSON protocol
  - Interactive mode with readline support and command history
  - Commands: `show all/workers/dirty/config/stats/listeners`
  - Worker management: `worker add/remove/kill`, `dirty add/remove`
  - Server control: `reload`, `reopen`, `shutdown`
  - New settings: `--control-socket`, `--control-socket-mode`, `--no-control-socket`
  - New CLI tool: `gunicornc` for connecting to control socket
  - See [Control Interface Guide](https://gunicorn.org/guides/gunicornc/) for details

- **Dirty Stash**: Add global shared state between workers via `dirty.stash`
  ([PR #&#8203;3503](benoitc/gunicorn#3503))
  - In-memory key-value store accessible by all workers
  - Supports get, set, delete, clear, keys, and has operations
  - Useful for sharing state like feature flags, rate limits, or cached data

- **Dirty Binary Protocol**: Implement efficient binary protocol for dirty arbiter IPC
  using TLV (Type-Length-Value) encoding
  ([PR #&#8203;3500](benoitc/gunicorn#3500))
  - More efficient than JSON for binary data
  - Supports all Python types: str, bytes, int, float, bool, None, list, dict
  - Better performance for large payloads

- **Dirty TTIN/TTOU Signals**: Add dynamic worker scaling for dirty arbiters
  ([PR #&#8203;3504](benoitc/gunicorn#3504))
  - Send SIGTTIN to increase dirty workers
  - Send SIGTTOU to decrease dirty workers
  - Respects minimum worker constraints from app configurations

##### Changes

- **ASGI Worker**: Promoted from beta to stable
- **Dirty Arbiters**: Now marked as beta feature

##### Documentation

- Fix Markdown formatting in /configure documentation

### [`v25.0.3`](https://github.com/benoitc/gunicorn/releases/tag/25.0.3)

[Compare Source](benoitc/gunicorn@25.0.2...25.0.3)

#### What's Changed

##### Bug Fixes

- Fix RuntimeError when StopIteration raised in ASGI coroutine ([#&#8203;3484](benoitc/gunicorn#3484))
- Fix passing maxsplit in re.split() as positional argument (deprecated in Python 3.13)

##### Documentation

- Updated sponsorship section and homepage

**Full Changelog**: <benoitc/gunicorn@25.0.2...25.0.3>

### [`v25.0.2`](https://github.com/benoitc/gunicorn/releases/tag/25.0.2)

[Compare Source](benoitc/gunicorn@25.0.1...25.0.2)

#### What's Changed

##### Bug Fixes

- Fix ASGI concurrent request failures through nginx proxy
- Graceful disconnect handling for ASGI worker
- Lazy import dirty module for gevent compatibility

##### Other

- Increase CI timeout for signal tests on PyPy
- Remove trailing blank line in instrument/**init**.py

**Full Changelog**: <benoitc/gunicorn@25.0.1...25.0.2>

### [`v25.0.1`](https://github.com/benoitc/gunicorn/releases/tag/25.0.1)

[Compare Source](benoitc/gunicorn@25.0.0...25.0.1)

#### Bug Fixes

- Fix ASGI streaming responses (SSE) hanging: add chunked transfer encoding for
  HTTP/1.1 responses without Content-Length header. Without chunked encoding,
  clients wait for connection close to determine end-of-response.

#### Changes

- Update celery\_alternative example to use FastAPI with native ASGI worker and
  uvloop for async task execution

#### Testing

- Add ASGI compliance test suite with Docker-based integration tests covering HTTP,
  WebSocket, streaming, lifespan, framework integration (Starlette, FastAPI),
  HTTP/2, and concurrency scenarios

### [`v25.0.0`](https://github.com/benoitc/gunicorn/releases/tag/25.0.0): Gunicorn 25.0.0

[Compare Source](benoitc/gunicorn@24.1.1...25.0.0)

#### New Features

- **Dirty Arbiters**: Separate process pool for executing long-running, blocking
  operations (AI model loading, heavy computation) without blocking HTTP workers
  ([PR #&#8203;3460](benoitc/gunicorn#3460))
  - Inspired by Erlang's dirty schedulers
  - Asyncio-based with Unix socket IPC
  - Stateful workers that persist loaded resources
  - New settings: `--dirty-app`, `--dirty-workers`, `--dirty-timeout`,
    `--dirty-threads`, `--dirty-graceful-timeout`
  - Lifecycle hooks: `on_dirty_starting`, `dirty_post_fork`,
    `dirty_worker_init`, `dirty_worker_exit`

- **Per-App Worker Allocation for Dirty Arbiters**: Control how many dirty workers
  load each app for memory optimization with heavy models
  ([PR #&#8203;3473](benoitc/gunicorn#3473))
  - Set `workers` class attribute on DirtyApp (e.g., `workers = 2`)
  - Or use config format `module:class:N` (e.g., `myapp:HeavyModel:2`)
  - Requests automatically routed to workers with the target app
  - New exception `DirtyNoWorkersAvailableError` for graceful error handling
  - Example: 8 workers × 10GB model = 80GB → with `workers=2`: 20GB (75% savings)

- **HTTP/2 Support (Beta)**: Native HTTP/2 (RFC 7540) support for improved performance
  with modern clients ([PR #&#8203;3468](benoitc/gunicorn#3468))
  - Multiplexed streams over a single connection
  - Header compression (HPACK)
  - Flow control and stream prioritization
  - Works with gthread, gevent, and ASGI workers
  - New settings: `--http-protocols`, `--http2-max-concurrent-streams`,
    `--http2-initial-window-size`, `--http2-max-frame-size`, `--http2-max-header-list-size`
  - Requires SSL/TLS and h2 library: `pip install gunicorn[http2]`
  - New example: `examples/http2_gevent/` with Docker and tests

- **HTTP 103 Early Hints**: Support for RFC 8297 Early Hints to enable browsers to
  preload resources before the final response
  ([PR #&#8203;3468](benoitc/gunicorn#3468))
  - WSGI: `environ['wsgi.early_hints'](headers)` callback
  - ASGI: `http.response.informational` message type
  - Works with both HTTP/1.1 and HTTP/2

- **uWSGI Protocol for ASGI Worker**: The ASGI worker now supports receiving requests
  via the uWSGI binary protocol from nginx
  ([PR #&#8203;3467](benoitc/gunicorn#3467))

#### Bug Fixes

- Fix HTTP/2 ALPN negotiation for gevent and eventlet workers when
  `do_handshake_on_connect` is False (the default). The TLS handshake is now
  explicitly performed before checking `selected_alpn_protocol()`.

- Fix setproctitle initialization with systemd socket activation
  ([#&#8203;3465](benoitc/gunicorn#3465))

- Fix `Expect: 100-continue` handling: ignore the header for HTTP/1.0 requests
  since 100-continue is only valid for HTTP/1.1+
  ([PR #&#8203;3463](benoitc/gunicorn#3463))

- Fix missing `_expected_100_continue` attribute in UWSGIRequest

- Disable setproctitle on macOS to prevent segfaults during process title updates

- Publish full exception traceback when the application fails to load
  ([#&#8203;3462](benoitc/gunicorn#3462))

- Fix ASGI: quick shutdown on SIGINT/SIGQUIT, graceful on SIGTERM

#### Deprecations

- **Eventlet Worker**: The `eventlet` worker is deprecated and will be removed in
  Gunicorn 26.0. Eventlet itself is no longer actively maintained.
  Please migrate to `gevent`, `gthread`, or another supported worker type.

#### Changes

- Remove obsolete Makefile targets
  ([PR #&#8203;3471](benoitc/gunicorn#3471))
- Replace RST with markdown documentation format

### [`v24.1.1`](https://github.com/benoitc/gunicorn/releases/tag/24.1.1)

[Compare Source](benoitc/gunicorn@24.1.0...24.1.1)

#### Bug Fixes

- Fix `forwarded_allow_ips` and `proxy_allow_ips` to remain as strings for backward
  compatibility with external tools like uvicorn. Network validation now uses strict
  mode to detect invalid CIDR notation (e.g., `192.168.1.1/24` where host bits are set)
  ([#&#8203;3458](benoitc/gunicorn#3458),
  [PR #&#8203;3459](benoitc/gunicorn#3459))

***

**Full Changelog**: <benoitc/gunicorn@24.1.0...24.1.1>

### [`v24.1.0`](https://github.com/benoitc/gunicorn/releases/tag/24.1.0): Gunicorn 24.1.0

[Compare Source](benoitc/gunicorn@24.0.0...24.1.0)

#### New Features

- **Official Docker Image**: Gunicorn now publishes official Docker images to GitHub Container Registry ([PR #&#8203;3454](benoitc/gunicorn#3454))
  - Available at `ghcr.io/benoitc/gunicorn`
  - Based on Python 3.12 slim image
  - Uses recommended worker formula (2 × CPU + 1)
  - Configurable via environment variables

- **PROXY Protocol v2 Support**: Extended PROXY protocol implementation to support the binary v2 format in addition to the existing text-based v1 format ([PR #&#8203;3451](benoitc/gunicorn#3451))
  - New `--proxy-protocol` modes: `off`, `v1`, `v2`, `auto`
  - `auto` mode (default when enabled) detects v1 or v2 automatically
  - v2 binary format is more efficient and supports additional metadata
  - Works with HAProxy, AWS NLB/ALB, and other PROXY protocol v2 sources

- **CIDR Network Support**: `--forwarded-allow-ips` and `--proxy-allow-from` now accept CIDR notation (e.g., `192.168.0.0/16`) for specifying trusted networks ([PR #&#8203;3449](benoitc/gunicorn#3449))

- **Socket Backlog Metric**: New `gunicorn.socket.backlog` gauge metric reports the current socket backlog size on Linux systems ([PR #&#8203;3450](benoitc/gunicorn#3450))

- **InotifyReloader Enhancement**: The inotify-based reloader now watches newly imported modules, not just those loaded at startup ([PR #&#8203;3447](benoitc/gunicorn#3447))

#### Bug Fixes

- Fix signal handling regression where SIGCLD alias caused "Unhandled signal: cld" errors on Linux when workers fail during boot ([#&#8203;3453](benoitc/gunicorn#3453))
- Fix socket blocking mode on keepalive connections preventing SSL handshake failures with async workers ([PR #&#8203;3452](benoitc/gunicorn#3452))
- Use smaller buffer size in `finish_body()` for faster timeout detection on slow or abandoned connections ([PR #&#8203;3453](benoitc/gunicorn#3453))
- Handle `SSLWantReadError` in `finish_body()` to prevent worker hangs during SSL renegotiation ([PR #&#8203;3448](benoitc/gunicorn#3448))
- Log SIGTERM as info level instead of warning to reduce noise in orchestrated environments ([PR #&#8203;3446](benoitc/gunicorn#3446))
- Print exception details to stderr when worker fails to boot ([PR #&#8203;3443](benoitc/gunicorn#3443))
- Fix `unreader.unread()` to prepend data to buffer instead of appending ([PR #&#8203;3442](benoitc/gunicorn#3442))
- Prevent `RecursionError` when pickling Config objects ([PR #&#8203;3441](benoitc/gunicorn#3441))
- Use proper exception chaining with `raise from` in glogging.py ([PR #&#8203;3440](benoitc/gunicorn#3440))

#### Installation

```bash
pip install gunicorn==24.1.0
```

Or use the official Docker image:

```bash
docker pull ghcr.io/benoitc/gunicorn:24.1.0
```

### [`v24.0.0`](https://github.com/benoitc/gunicorn/releases/tag/24.0.0)

[Compare Source](benoitc/gunicorn@23.0.0...24.0.0)

#### New Features

- **ASGI Worker (Beta)**: Native asyncio-based ASGI support for running async Python frameworks like FastAPI, Starlette, and Quart without external dependencies
  - HTTP/1.1 with keepalive connections
  - WebSocket support
  - Lifespan protocol for startup/shutdown hooks
  - Optional uvloop for improved performance

- **uWSGI Binary Protocol**: Support for receiving requests from nginx via `uwsgi_pass` directive

- **Documentation Migration**: Migrated to MkDocs with Material theme

#### Security

- **eventlet**: Require eventlet >= 0.40.3 (CVE-2021-21419, CVE-2025-58068)
- **gevent**: Require gevent >= 24.10.1 (CVE-2023-41419, CVE-2024-3219)
- **tornado**: Require tornado >= 6.5.0 (CVE-2025-47287)

#### Install

```
pip install gunicorn==24.0.0
```

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC41Mi4wIiwidXBkYXRlZEluVmVyIjoiNDQuNTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=-->

Reviewed-on: https://git.tainton.uk/repos/rsu/pulls/16
Co-authored-by: renovate[bot] <renovate-bot@git.tainton.uk>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wsgi.early_hints callback has no header validation (response splitting risk)

3 participants