You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Symptom was investigated, root cause was found, and this issue was raised - all by Claude (with occasional (re)direction from me)
This note was authored by me
Description
When a client request arrives over HTTP/3 with no body (e.g. a GET) and reverse_proxy forwards it to the upstream over HTTP/2, Caddy sends the upstream request with an indeterminate body length (ContentLength: -1) instead of 0.
Lenient HTTP/2 upstreams tolerate it.
Strict HTTP/2 upstreams (e.g. IIS + Application Request Routing) reject the indeterminate-length request → HTTP 502. There it surfaces as an illegal Transfer-Encoding: chunked request header (forbidden in HTTP/2, RFC 9113 §8.1.1).
The same request is forwarded correctly (ContentLength: 0) when it originates over HTTP/1.1 or HTTP/2, or when the upstream hop is forced to HTTP/1.1.
This is a regression introduced in v2.9.0 (v2.8.4 and earlier are unaffected). It may originate in the bundled HTTP/3 (quic-go) request handling.
Reproduction (two Caddy instances, no other dependencies)
Send a bodyless GET to the front, once over HTTP/3 and once over HTTP/2, and compare what the upstream reports:
client protocol
upstream sees
HTTP/3
content-length=-1 ❌
HTTP/2
content-length=0 ✅
Add versions 1.1 to the front's transport http block and both become content-length=0 — confirming the trigger is HTTP/2 to the upstream.
Version bisect
version
result
version
result
2.7.6
OK
2.9.1
broken
2.8.4
OK
2.10.0
broken
2.9.0
broken (first)
2.11.4
broken
Real-world impact
Browser (HTTP/3) → Caddy → IIS/ARR upstream (HTTP/2). ARR returns 502.7 (0x80070057, E_INVALIDARG) for every proxied asset. IIS Failed Request Tracing shows the inbound request carrying Transfer-Encoding: chunked.
Workarounds
transport http { versions 1.1 } on the upstream, or
disable HTTP/3 on the front.
Environment
Caddy v2.11.4 (also reproduced on 2.9.0 / 2.9.1 / 2.10.0), Windows amd64.
Client used for the bisect: .NET HttpClient with Version = 3.0 and VersionPolicy = RequestVersionExact (forces HTTP/3); any HTTP/3 client sending a bodyless GET reproduces it.
Important
Description
When a client request arrives over HTTP/3 with no body (e.g. a
GET) andreverse_proxyforwards it to the upstream over HTTP/2, Caddy sends the upstream request with an indeterminate body length (ContentLength: -1) instead of0.Transfer-Encoding: chunkedrequest header (forbidden in HTTP/2, RFC 9113 §8.1.1).The same request is forwarded correctly (
ContentLength: 0) when it originates over HTTP/1.1 or HTTP/2, or when the upstream hop is forced to HTTP/1.1.This is a regression introduced in v2.9.0 (v2.8.4 and earlier are unaffected). It may originate in the bundled HTTP/3 (quic-go) request handling.
Reproduction (two Caddy instances, no other dependencies)
upstream — echoes what it received:
front — HTTP/3 enabled (default), HTTP/2 to the upstream:
Send a bodyless GET to the front, once over HTTP/3 and once over HTTP/2, and compare what the upstream reports:
content-length=-1❌content-length=0✅Add
versions 1.1to the front'stransport httpblock and both becomecontent-length=0— confirming the trigger is HTTP/2 to the upstream.Version bisect
Real-world impact
Browser (HTTP/3) → Caddy → IIS/ARR upstream (HTTP/2). ARR returns
502.7(0x80070057, E_INVALIDARG) for every proxied asset. IIS Failed Request Tracing shows the inbound request carryingTransfer-Encoding: chunked.Workarounds
transport http { versions 1.1 }on the upstream, orEnvironment
HttpClientwithVersion = 3.0andVersionPolicy = RequestVersionExact(forces HTTP/3); any HTTP/3 client sending a bodyless GET reproduces it.