Skip to content

Bump gunicorn from 23.0.0 to 26.1.0 - #38074

Open
dependabot[bot] wants to merge 1 commit into
masterfrom
dependabot/uv/gunicorn-26.1.0
Open

Bump gunicorn from 23.0.0 to 26.1.0#38074
dependabot[bot] wants to merge 1 commit into
masterfrom
dependabot/uv/gunicorn-26.1.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 27, 2026

Copy link
Copy Markdown
Contributor

Bumps gunicorn from 23.0.0 to 26.1.0.

Release notes

Sourced from gunicorn's releases.

gunicorn 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 (#1643, #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 (#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 (#3382, #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

... (truncated)

Commits
  • 71b59a7 Merge pull request #3698 from benoitc/fix/docker-health-check-readerror
  • 48287de test: catch every transport error in the docker health check
  • 3110e8c Merge pull request #3696 from benoitc/docs/roadmap
  • cc56c41 Merge pull request #3693 from benoitc/release/26.1.0
  • 5cf1f16 docs: surface the roadmap on the site home page
  • 7e35f72 docs: add FastCGI to the roadmap and point items at Ideas
  • 18ddc58 docs: drop the framework and reverse-proxy non-goals from the roadmap
  • 1ecae56 docs: add a roadmap and make the chat easy to find
  • ca412e3 docs: sync the Latest changelog page with 26.1.0
  • 640936f docs: note the dependency security work in 26.1.0
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 23.0.0 to 26.1.0.
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](benoitc/gunicorn@23.0.0...26.1.0)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-version: 26.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added the product/invisible Change has no end-user visible impact label Aug 27, 2026
@dependabot
dependabot Bot requested a review from a team as a code owner August 27, 2026 10:08
@dependabot dependabot Bot added the product/invisible Change has no end-user visible impact label Aug 27, 2026
@dimagimon dimagimon added the dependencies Pull requests that update a dependency file label Aug 27, 2026
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown

🔍 Dependency Analysis Summary

A lockfile-only bump carrying gunicorn across three major versions (23 → 24 → 25 → 26). pyproject.toml declares an unpinned "gunicorn", so nothing else in the repo changes — but this is the WSGI server terminating every production request, and 26.0 both removed a worker class and materially tightened HTTP/1.1 parsing. Upstream also restructured its metadata so all runtime dependencies are now optional extras.

Overall risk: MEDIUM — nothing HQ uses is broken, but the wire-protocol changes and the extras restructure deserve a look before production.

📋 Detailed Changelog Review

gunicorn 23.0.0 → 26.1.0 — Changes

  • 24.0.0 — Native asyncio ASGI worker (beta); uWSGI binary protocol. Raised floors for optional worker backends (gevent >= 24.10.1, tornado >= 6.5.0).
  • 25.0.0HTTP/2 (beta, needs TLS + the http2 extra), HTTP 103 Early Hints, "dirty arbiters" for long-running blocking work (--dirty-workers/--dirty-timeout). Deprecated eventlet. Fixed HTTP/2 ALPN negotiation for the gevent worker.
  • 26.0.0 — Dropped the eventlet worker. Much stricter HTTP/1.1 conformance: request-target validation (invalid authority and asterisk forms rejected), header hardening per RFC 9110 (control chars rejected), duplicate Host/Content-Type rejected, tightened keepalive gating, PROXY-protocol source enforcement, RFC 9112 connection draining. Body-framing fixes for HEAD/204/304.
  • 26.1.0 — Glob patterns in reload_extra_files. SIGHUP now re-runs logger setup, so logconfig*/loglevel changes apply on reload instead of needing a full restart (invalid config rolls back, non-fatal). Chunked bodies truncated right after the final 0 chunk line are now rejected with ChunkMissingTerminator instead of accepted as complete.

Wheel grew 85 KB → 216 KB. None of the new surface is on by default.

Breaking Changes

  1. eventlet worker removed (26.0). Not applicable — deployment/gunicorn/gunicorn_conf.py:5 sets worker_class = "gevent".
  2. Stricter HTTP/1.1 parsing (26.0, 26.1). Previously-tolerated requests — duplicate Host/Content-Type, control chars in headers, malformed request-targets, chunked bodies missing the mandatory empty trailer — are now rejected. Most likely source of a real behavior difference.
  3. Metadata restructured: 26.1.0 declares zero unconditional dependencies; all moved behind extras (gevent, tornado, http2, fast, setproctitle). Visible in the diff — the dependencies = [{ name = "packaging" }] block is deleted, not replaced.

Migration Notes

The extras restructure is what needs a decision. The gevent extra is now gevent>=24.10.1 plus packaging, and gunicorn/workers/ggevent.py uses both at import time (from packaging.version import parse as parse_version, then RuntimeError("gevent worker requires gevent 24.10.1 or higher")). Both fire during module import, before any worker starts.

HQ satisfies them only incidentally, by happening to declare both directly: gevent>=24.11.1 (clears the floor ✅) and packaging at pyproject.toml:81 ✅. Since pyproject.toml:61 requests plain "gunicorn" with no extras, dropping either direct dep later would break gevent startup with an error pointing at gunicorn rather than the removed package. gunicorn[gevent] makes it explicit and self-enforcing.

The 26.0.0 notes mislead on one point: they say "Require gunicorn_h1c >= 0.6.5", which reads as mandatory. It is not. PyPI lists gunicorn_h1c>=0.6.6; extra == "fast", and gunicorn/http/message.py wraps the import in try/except ImportError setting _fast_parser_available = False. HQ falls back to the pure-Python parser cleanly — no startup failure, just no C parser. gunicorn[fast] is an optional perf follow-up.

Python floor moved to >=3.10; HQ requires >=3.13, so a non-issue.

⚠️ Impact Assessment

Breaking Changes Found: Yes, but none affecting HQ's current configuration.

Every setting and hook in gunicorn_conf.py was checked against gunicorn/config.py at tag 26.1.0 — all unchanged in name, default, and semantics: preload_app, worker_class, keepalive, timeout, max_requests, max_requests_jitter, graceful_timeout, limit_request_line (HQ sets 4500; default still 4094), and the hooks post_fork(server, worker), on_starting(server), child_exit(server, worker).

Affected Files — none require changes. Touchpoints:

  • deployment/gunicorn/gunicorn_conf.py — verified compatible.
  • deployment/gunicorn/commcarehq_wsgi.py — gevent monkey-patching entrypoint; unaffected.
  • settings.py:232"gunicorn" in DEFAULT_APPS. Django imports the package for app-config discovery; gunicorn/__init__.py at 26.1.0 is still only version constants with no imports or side effects, so this stays safe.
  • pyproject.toml:61 — candidate for gunicorn[gevent].
  • manage.py:20GeventCommand("run_gunicorn") is pre-existing dead config, not a regression: gunicorn removed that command in v19 and nothing else references it.

SERVER_SOFTWARE / the Server: header becomes gunicorn/26.1.0.

Test Impact — coverage is thinner than it looks. testapps/test_gunicorn/tests.py exercises only HQ's own _on_starting/_child_exit against hand-rolled stubs; it never loads gunicorn, so it passes identically on 23 or 26 and proves nothing here. The test that genuinely exercises the new version is the import smoke test at scripts/test-prod-entrypoints.sh:5 (run from docker/run.sh:145), which would catch an import-time RuntimeError from the gevent/packaging checks. It catches nothing about request parsing.

Configuration Changes: None in this repo.

🛠️ Recommendations

Action Required

  1. Review commcare-cloud's supervisor args. gunicorn_conf.py:8-10 notes max_requests, max_requests_jitter, and graceful_timeout are overridden by CLI flags in the supervisor conf. Those flags still exist in 26.1.0, but the full production command line lives outside this repo and is worth a read-through across a three-major jump.
  2. Confirm PROXY-protocol / forwarded_allow_ips posture — 26.0 enforces the source-IP whitelist more strictly. HQ's config file does not set these, so this only matters if commcare-cloud does.

Testing Focus

  • Mobile form submissions — highest value. Mobile clients POST multipart payloads; 26.1 rejects chunked bodies missing the mandatory trailer, 26.0 rejects duplicate Content-Type. A previously-tolerated client now gets a 400. Soak on staging against real device traffic, not synthetic requests.
  • Long-running requests under geventtimeout = 900 and graceful_timeout = 300 are far outside defaults, and the GreenletExit workaround at gunicorn_conf.py:13 suggests this path has been fragile. Verify draining given the RFC 9112 changes.
  • preload_app + the post_fork URL-resolution hack (gunicorn_conf.py:19-23) — confirm it still works on the new arbiter.
  • Prometheus multiprocess metrics — confirm on_starting/child_exit cleanup after a real worker recycle.
  • Log output and graceful reload — SIGHUP now rebuilds logger config; check reloads keep the expected format and do not duplicate handlers.

Follow-up Tasks

  • Declare gunicorn[gevent] in pyproject.toml so the packaging and gevent-floor requirements are explicit rather than incidental.
  • Evaluate gunicorn[fast] (the gunicorn_h1c C parser) as a throughput win.
  • Remove the vestigial GeventCommand("run_gunicorn") at manage.py:20.

Merge Recommendation: REVIEW_NEEDED — static analysis is clean (no removed settings, no removed hooks, correct worker class, dependency floors satisfied). What holds it back from a straight approve is three majors of change to the component parsing every inbound request; the stricter HTTP/1.1 enforcement can only really be validated against real client traffic. Merge after a staging soak with mobile submissions.

📚 Useful Links

Based on upstream release notes, PyPI metadata, and reading gunicorn 26.1.0 source (config.py, workers/ggevent.py, http/message.py, __init__.py) against HQ's configuration. No runtime verification against the upgraded package was performed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file product/invisible Change has no end-user visible impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant