Skip to content

reverse_proxy: bodyless HTTP/3 client GET forwarded to an HTTP/2 upstream gets ContentLength -1 instead of 0 (regression in v2.9.0) #7835

Description

@ChintanRaval

Important

  • 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)

upstream — echoes what it received:

https://up.example.test:9500 {
	tls internal
	templates
	respond "proto={{.Req.Proto}} transfer-encoding={{.Req.TransferEncoding}} content-length={{.Req.ContentLength}}" 200
}

front — HTTP/3 enabled (default), HTTP/2 to the upstream:

https://front.example.test:8443 {
	tls internal
	reverse_proxy https://up.example.test:9500 {
		transport http {
			tls_insecure_skip_verify
		}
	}
}

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🐞Something isn't workingupstream ⬆️Relates to some dependency of this project

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions