Issue Details
When an upstream responds with a non-zero Content-Length but then closes the connection before sending the content (or sends too small amount of content), Caddy will drop the connection and the client will not receive anything, instead the client connection is abruptly closed.
I'm assuming that Caddy is doing some buffering, realizes the mismatch, and drops everything. However I would expect caddy to be a little more transparent: as long as the response had fully formed headers caddy should pass it along.
To reproduce:
reverse_proxy 10.0.0.2:80 and cause the backend to issue HTTP 500 with Content-Length 123 but then close the connection right after the headers.
Curl sessions
Direct backend response (what I would expect caddy to pass along)
curl http://example.com/demo.mkv -v --resolve example.com:80:10.0.0.8
* Added example.com:80:10.0.0.8 to DNS cache
* Hostname example.com was found in DNS cache
* Trying 10.0.0.8:80...
* Connected to example.com (10.0.0.8) port 80
* using HTTP/1.x
> GET /demo.mkv HTTP/1.1
> Host: example.com
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 500 Internal Server Error
< Server: nginx
< Date: Sun, 28 Jun 2026 16:13:57 GMT
< Content-Type: video/x-matroska
< Content-Length: 135708
< Connection: keep-alive
< Content-Disposition: inline; filename="demo.mkv"
< Accept-Ranges: bytes
<
* end of response with 135708 bytes missing
* closing connection #0
curl: (18) end of response with 135708 bytes missing
What caddy responds instead:
curl https://example.com/demo.mkv -v
* Host example.com:443 was resolved.
* IPv6: (none)
* IPv4: 1.2.3.4
* Trying 1.2.3.4:443...
* ALPN: curl offers h2,http/1.1
[TLS STUFF...]
* Connected to example.com (1.2.3.4) port 443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://example.com/demo.mkv
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: example.com]
* [HTTP/2] [1] [:path: /demo.mkv]
* [HTTP/2] [1] [user-agent: curl/8.14.1]
* [HTTP/2] [1] [accept: */*]
> GET /demo.mkv HTTP/2
> Host: example.com
> User-Agent: curl/8.14.1
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Request completely sent off
* HTTP/2 stream 1 was not closed cleanly: INTERNAL_ERROR (err 2)
* Connection #0 to host example.com left intact
curl: (92) HTTP/2 stream 1 was not closed cleanly: INTERNAL_ERROR (err 2)
To make sure it wasn't HTTP2 weirdness, I forced caddy to respond in http 1.1 without tls:
curl http://example.com/demo.mkv -v
* Host example.com:80 was resolved.
* IPv6: (none)
* IPv4: 1.2.3.4
* Trying 1.2.3.4:80...
* Connected to example.com (1.2.3.4) port 80
* using HTTP/1.x
> GET /demo.mkv HTTP/1.1
> Host: example.com
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
* Empty reply from server
* shutting down connection #0
curl: (52) Empty reply from server
The stderr output
{"level":"warn","ts":1782663200.3506744,"logger":"http.handlers.reverse_proxy","msg":"aborting with incomplete response","upstream":"10.0.0.8:80","duration":0.000743837,"request":{"remote_ip":"10.0.0.1","remote_port":"35568","client_ip":"10.0.0.1","proto":"HTTP/1.1","method":"GET","host":"example.com","uri":"/demo.mkv","headers":{"X-Forwarded-Proto":["http"],"X-Forwarded-Host":["example.com"],"Via":["1.1 Caddy"],"Accept":["*/*"],"User-Agent":["curl/8.14.1"],"X-Forwarded-For":["10.0.0.1"]}},"error":"reading: unexpected EOF"}
Assistance Disclosure
AI not used
If AI was used, describe the extent to which it was used.
No response
Issue Details
When an upstream responds with a non-zero Content-Length but then closes the connection before sending the content (or sends too small amount of content), Caddy will drop the connection and the client will not receive anything, instead the client connection is abruptly closed.
I'm assuming that Caddy is doing some buffering, realizes the mismatch, and drops everything. However I would expect caddy to be a little more transparent: as long as the response had fully formed headers caddy should pass it along.
To reproduce:
reverse_proxy 10.0.0.2:80and cause the backend to issue HTTP 500 with Content-Length 123 but then close the connection right after the headers.Curl sessions
Direct backend response (what I would expect caddy to pass along)
What caddy responds instead:
To make sure it wasn't HTTP2 weirdness, I forced caddy to respond in http 1.1 without tls:
The stderr output
Assistance Disclosure
AI not used
If AI was used, describe the extent to which it was used.
No response