mirror of
https://github.com/caddyserver/caddy.git
synced 2026-10-05 12:21:36 -04:00
A request whose body has no length of its own — Transfer-Encoding: chunked, or HTTP/2 and HTTP/3 requests sent without the header — cannot be forwarded over FastCGI until it has been buffered in full, because CGI/1.1 requires CONTENT_LENGTH and php-fpm hangs when it is absent or wrong and the body is not empty. When request_buffers is too small to hold the whole body, the length stays unknown and the request is refused with 411. The refusal said none of that. RoundTrip passes r.ContentLength to Post, which is -1 for such a request, so the operator got a bare "411 Length Required" with either no error at all or strconv's "invalid syntax" on a value they never wrote. On #7386 that sent two people looking for the fault in their backend. The 411 now carries the reason and the remedy, and the unknown-length case is told apart from a genuinely malformed value. ParseUint becomes ParseInt plus an explicit negative check, which is strictly tighter: values above MaxInt64 used to be accepted and are unreachable anyway, since CONTENT_LENGTH is always FormatInt of an int64 by the time Do sees it. No status code changes. Every request that was refused before is still refused; raising request_buffers is still what makes a large chunked body work. Tests: the reachable path through Post, the unusable CONTENT_LENGTH values Do itself guards against, an integration test driving a chunked body through a fastcgi reverse_proxy over a unix socket at each side of the buffer boundary (including request_buffers -1, which succeeds at any size and is the remedy), and a boundary test recording that a body exactly the size of the buffer counts as partial. That last one is deliberately not changed here. Telling "exactly the limit" apart from "more to come" costs a read that a paused stream may never answer, and bufferedBody also backs response_buffers: measured against a body that delivers exactly the limit and stops, the current code hands the buffered prefix on immediately, while peeking one byte first delivers nothing at all. Co-authored-by: Aditya <205600203+Rohilalala@users.noreply.github.com> Co-authored-by: Zen Dodd <mail@steadytao.com>
124 lines
3.3 KiB
Go
124 lines
3.3 KiB
Go
package reverseproxy
|
|
|
|
import (
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type zeroReader struct{}
|
|
|
|
func (zeroReader) Read(p []byte) (int, error) {
|
|
for i := range p {
|
|
p[i] = 0
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func TestBuffering(t *testing.T) {
|
|
var (
|
|
h Handler
|
|
zr zeroReader
|
|
)
|
|
type args struct {
|
|
body io.ReadCloser
|
|
limit int64
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
resultCheck func(io.ReadCloser, int64, args) bool
|
|
}{
|
|
{
|
|
name: "0 limit, body is returned as is",
|
|
args: args{
|
|
body: io.NopCloser(&zr),
|
|
limit: 0,
|
|
},
|
|
resultCheck: func(res io.ReadCloser, read int64, args args) bool {
|
|
return res == args.body && read == args.limit && read == 0
|
|
},
|
|
},
|
|
{
|
|
name: "negative limit, body is read completely",
|
|
args: args{
|
|
body: io.NopCloser(io.LimitReader(&zr, 100)),
|
|
limit: -1,
|
|
},
|
|
resultCheck: func(res io.ReadCloser, read int64, args args) bool {
|
|
brc, ok := res.(bodyReadCloser)
|
|
return ok && brc.body == nil && brc.buf.Len() == 100 && read == 100
|
|
},
|
|
},
|
|
{
|
|
name: "positive limit, body is read partially",
|
|
args: args{
|
|
body: io.NopCloser(io.LimitReader(&zr, 100)),
|
|
limit: 50,
|
|
},
|
|
resultCheck: func(res io.ReadCloser, read int64, args args) bool {
|
|
brc, ok := res.(bodyReadCloser)
|
|
return ok && brc.body != nil && brc.buf.Len() == 50 && read == 50
|
|
},
|
|
},
|
|
{
|
|
name: "positive limit, body is read completely",
|
|
args: args{
|
|
body: io.NopCloser(io.LimitReader(&zr, 100)),
|
|
limit: 101,
|
|
},
|
|
resultCheck: func(res io.ReadCloser, read int64, args args) bool {
|
|
brc, ok := res.(bodyReadCloser)
|
|
return ok && brc.body == nil && brc.buf.Len() == 100 && read == 100
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
res, read := h.bufferedBody(tt.args.body, tt.args.limit)
|
|
if !tt.resultCheck(res, read, tt.args) {
|
|
t.Error("Handler.bufferedBody() test failed")
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBufferingBoundary documents where a buffered body stops being fully
|
|
// buffered, which is what decides whether the caller can learn its length and
|
|
// set Content-Length. A body the size of the limit counts as partial: io.CopyN
|
|
// filling the buffer cannot be told apart from a body with more to come, and
|
|
// finding out would mean a blocking read on a stream that may never deliver.
|
|
func TestBufferingBoundary(t *testing.T) {
|
|
const body = "0123456789"
|
|
|
|
for limit := int64(1); limit <= int64(len(body))+2; limit++ {
|
|
var h Handler
|
|
res, read := h.bufferedBody(io.NopCloser(strings.NewReader(body)), limit)
|
|
|
|
// Whatever the limit, the upstream must still receive every byte.
|
|
got, err := io.ReadAll(res)
|
|
if err != nil {
|
|
t.Fatalf("limit %d: reading buffered body: %v", limit, err)
|
|
}
|
|
if string(got) != body {
|
|
t.Errorf("limit %d: body changed: got %q, want %q", limit, got, body)
|
|
}
|
|
if err := res.Close(); err != nil {
|
|
t.Errorf("limit %d: closing buffered body: %v", limit, err)
|
|
}
|
|
|
|
brc, ok := res.(bodyReadCloser)
|
|
if !ok {
|
|
t.Fatalf("limit %d: expected a bodyReadCloser", limit)
|
|
}
|
|
wantFull := limit > int64(len(body))
|
|
if gotFull := brc.body == nil; gotFull != wantFull {
|
|
t.Errorf("limit %d: fully buffered = %v, want %v", limit, gotFull, wantFull)
|
|
}
|
|
if wantFull && read != int64(len(body)) {
|
|
t.Errorf("limit %d: read %d bytes, want %d", limit, read, len(body))
|
|
}
|
|
}
|
|
}
|