mirror of
https://github.com/caddyserver/caddy.git
synced 2026-10-31 16:32:45 -04:00
686 lines
24 KiB
Go
686 lines
24 KiB
Go
package reverseproxy
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
|
)
|
|
|
|
// testProxyStatusName identifies the proxy in Proxy-Status assertions. RFC 9209
|
|
// wants the deployment named rather than the software, so the tests configure
|
|
// one the way a deployment would.
|
|
const testProxyStatusName = "proxy-3.example.com"
|
|
|
|
// assertRefused checks the response of a refused incremental forwarding
|
|
// against what RFC 10036 section 4.1 recommends.
|
|
func assertRefused(t *testing.T, err error, hdr http.Header) {
|
|
t.Helper()
|
|
|
|
var handlerErr caddyhttp.HandlerError
|
|
if !errors.As(err, &handlerErr) {
|
|
t.Fatalf("expected a caddyhttp.HandlerError, got %T: %v", err, err)
|
|
}
|
|
if handlerErr.StatusCode != http.StatusNotImplemented {
|
|
t.Errorf("status = %d, want %d", handlerErr.StatusCode, http.StatusNotImplemented)
|
|
}
|
|
|
|
want := testProxyStatusName + ";error=" + proxyErrorIncrementalRefused
|
|
if got := hdr.Get("Proxy-Status"); got != want {
|
|
t.Errorf("Proxy-Status = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// incrementalHandler proxies to backendAddr under a name it can report in a
|
|
// Proxy-Status field.
|
|
func incrementalHandler(backendAddr string) *Handler {
|
|
h := minimalHandler(0, &Upstream{
|
|
Host: new(Host),
|
|
Dial: backendAddr,
|
|
})
|
|
h.ProxyStatusName = testProxyStatusName
|
|
|
|
return h
|
|
}
|
|
|
|
// incrementalRequest builds a request marked incremental; an unknown length is
|
|
// what a chunked request looks like once the server has parsed it.
|
|
func incrementalRequest(body string, knownLength bool) *http.Request {
|
|
var r io.Reader = strings.NewReader(body)
|
|
if !knownLength {
|
|
r = io.NopCloser(r) // opaque to httptest, so ContentLength stays -1
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "http://example.com/", r)
|
|
req.Header.Set("Incremental", "?1")
|
|
return prepareTestRequest(req)
|
|
}
|
|
|
|
// lengthRequiringTransport stands in for fastcgi, which cannot forward a body
|
|
// whose length it does not know.
|
|
type lengthRequiringTransport struct{ testTransport }
|
|
|
|
func (lengthRequiringTransport) RequiresContentLength() bool { return true }
|
|
|
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
return f(r)
|
|
}
|
|
|
|
type lengthRequiringRoundTripFunc roundTripFunc
|
|
|
|
func (f lengthRequiringRoundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
return roundTripFunc(f)(r)
|
|
}
|
|
|
|
func (lengthRequiringRoundTripFunc) RequiresContentLength() bool { return true }
|
|
|
|
type stagedBody struct {
|
|
release <-chan struct{}
|
|
sent bool
|
|
}
|
|
|
|
func (b *stagedBody) Read(p []byte) (int, error) {
|
|
if !b.sent {
|
|
b.sent = true
|
|
p[0] = 'x'
|
|
return 1, nil
|
|
}
|
|
<-b.release
|
|
return 0, io.EOF
|
|
}
|
|
|
|
func (*stagedBody) Close() error { return nil }
|
|
|
|
// Without buffering or a transport framing conflict, an unknown-length
|
|
// incremental request is streamed unchanged.
|
|
func TestIncrementalProxiedWhenRequestLengthUnknown(t *testing.T) {
|
|
var gotBody string
|
|
var gotLength int64
|
|
var gotIncremental string
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
gotBody = string(b)
|
|
gotLength = r.ContentLength
|
|
gotIncremental = r.Header.Get("Incremental")
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
|
|
rec := httptest.NewRecorder()
|
|
if err := h.ServeHTTP(rec, incrementalRequest("hello", false), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
})); err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
|
|
if gotBody != "hello" {
|
|
t.Errorf("upstream body = %q, want %q", gotBody, "hello")
|
|
}
|
|
if gotLength != -1 {
|
|
t.Errorf("upstream ContentLength = %d, want -1", gotLength)
|
|
}
|
|
if gotIncremental != "?1" {
|
|
t.Errorf("upstream Incremental = %q, want ?1", gotIncremental)
|
|
}
|
|
}
|
|
|
|
// A transport that needs a CONTENT_LENGTH is the exception: only buffering the
|
|
// body in full can supply one, which is what incremental forwarding rules out.
|
|
func TestIncrementalRefusedWhenTransportRequiresContentLength(t *testing.T) {
|
|
for _, requestBuffers := range []int64{0, 4096} {
|
|
t.Run(fmt.Sprintf("buffers_%d", requestBuffers), func(t *testing.T) {
|
|
var reached bool
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.RequestBuffers = requestBuffers
|
|
h.Transport = lengthRequiringTransport{testTransport{&http.Transport{}}}
|
|
|
|
rec := httptest.NewRecorder()
|
|
err := h.ServeHTTP(rec, incrementalRequest("hello", false), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
|
|
assertRefused(t, err, rec.Header())
|
|
if reached {
|
|
t.Error("request was forwarded upstream despite being refused")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// An unlimited buffer holds the entire body no matter its length, which RFC
|
|
// 10036 section 3 forbids for a message marked incremental.
|
|
func TestIncrementalRefusedWhenRequestBufferUnlimited(t *testing.T) {
|
|
var reached bool
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.RequestBuffers = -1
|
|
|
|
rec := httptest.NewRecorder()
|
|
err := h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
|
|
assertRefused(t, err, rec.Header())
|
|
if reached {
|
|
t.Error("request was forwarded upstream despite being refused")
|
|
}
|
|
}
|
|
|
|
// An explicit buffer is operator policy. An incremental request is refused
|
|
// instead of allowing an external header to disable that policy.
|
|
func TestIncrementalRefusedWhenRequestBufferConfigured(t *testing.T) {
|
|
h := incrementalHandler("unused.invalid")
|
|
h.RequestBuffers = 4096
|
|
rec := httptest.NewRecorder()
|
|
err := h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
assertRefused(t, err, rec.Header())
|
|
}
|
|
|
|
// A transport-supplied buffer that exists only to discover Content-Length is
|
|
// unnecessary when the request already has one and must not delay forwarding.
|
|
func TestIncrementalKnownLengthRequestBypassesTransportDefault(t *testing.T) {
|
|
release := make(chan struct{})
|
|
reached := make(chan struct{})
|
|
h := incrementalHandler("unused.invalid")
|
|
h.RequestBuffers = 4096
|
|
h.requestBuffersFromTransport = true
|
|
h.Transport = lengthRequiringRoundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
close(reached)
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: http.NoBody,
|
|
ContentLength: 0,
|
|
Request: req,
|
|
}, nil
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "http://example.com/", &stagedBody{release: release})
|
|
req.ContentLength = 2
|
|
req.Header.Set("Content-Length", "2")
|
|
req.Header.Set("Incremental", "?1")
|
|
req = prepareTestRequest(req)
|
|
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- h.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
}()
|
|
|
|
select {
|
|
case <-reached:
|
|
case <-time.After(2 * time.Second):
|
|
t.Error("incremental request remained in the transport's default buffer")
|
|
}
|
|
close(release)
|
|
if err := <-done; err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
}
|
|
|
|
// Without buffering configured there is nothing to refuse, so the request is
|
|
// proxied as usual.
|
|
func TestIncrementalProxiedWhenRequestNotBuffered(t *testing.T) {
|
|
var gotBody string
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
gotBody = string(b)
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
|
|
rec := httptest.NewRecorder()
|
|
if err := h.ServeHTTP(rec, incrementalRequest("hello", false), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
})); err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
|
|
if gotBody != "hello" {
|
|
t.Errorf("upstream body = %q, want %q", gotBody, "hello")
|
|
}
|
|
}
|
|
|
|
// A bodyless request has no content to forward incrementally, so buffering it
|
|
// is a no-op and there is nothing to refuse.
|
|
func TestIncrementalProxiedWhenRequestHasNoBody(t *testing.T) {
|
|
var reached bool
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.RequestBuffers = -1
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
|
|
req.Header.Set("Incremental", "?1")
|
|
req = prepareTestRequest(req)
|
|
|
|
rec := httptest.NewRecorder()
|
|
if err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
})); err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
|
|
if !reached {
|
|
t.Error("bodyless request was not forwarded upstream")
|
|
}
|
|
}
|
|
|
|
// A configured method rewrite can remove the request content before proxying.
|
|
// Refusal is decided after that rewrite rather than from the original request.
|
|
func TestIncrementalRewriteRemovesRequestContentBeforeRefusal(t *testing.T) {
|
|
var gotLength int64 = -1
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotLength = r.ContentLength
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.RequestBuffers = -1
|
|
h.Rewrite = &rewrite.Rewrite{Method: http.MethodGet}
|
|
|
|
rec := httptest.NewRecorder()
|
|
if err := h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
})); err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
if gotLength != 0 {
|
|
t.Errorf("upstream ContentLength = %d, want 0", gotLength)
|
|
}
|
|
}
|
|
|
|
func TestIncrementalAddedByHeaderUpAfterBufferingIsRefused(t *testing.T) {
|
|
h := incrementalHandler("unused.invalid")
|
|
h.RequestBuffers = 4096
|
|
h.Headers = &headers.Handler{Request: &headers.HeaderOps{
|
|
Set: http.Header{"Incremental": []string{"?1"}},
|
|
}}
|
|
req := incrementalRequest("hello", true)
|
|
req.Header.Del("Incremental")
|
|
|
|
rec := httptest.NewRecorder()
|
|
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
assertRefused(t, err, rec.Header())
|
|
}
|
|
|
|
// serveIncremental runs an ordinary request against a backend whose response
|
|
// is marked incremental. Request and response signals are independent.
|
|
func serveIncremental(t *testing.T, responseBuffers int64, backendFn http.HandlerFunc) (*httptest.ResponseRecorder, error) {
|
|
t.Helper()
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Incremental", "?1")
|
|
backendFn(w, r)
|
|
}))
|
|
t.Cleanup(backend.Close)
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.ResponseBuffers = responseBuffers
|
|
|
|
req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "http://example.com/", nil))
|
|
rec := httptest.NewRecorder()
|
|
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
return rec, err
|
|
}
|
|
|
|
// An explicit response buffer is operator policy. An incremental response is
|
|
// refused rather than allowing the upstream to disable that policy.
|
|
func TestIncrementalRefusedWhenResponseBufferConfigured(t *testing.T) {
|
|
rec, err := serveIncremental(t, 4096, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.(http.Flusher).Flush() // forces chunked, so the length stays unknown
|
|
_, _ = w.Write([]byte("data: hi\n\n"))
|
|
})
|
|
assertRefused(t, err, rec.Header())
|
|
}
|
|
|
|
// An unlimited buffer holds the entire response, whatever its length.
|
|
func TestIncrementalRefusedWhenResponseBufferUnlimited(t *testing.T) {
|
|
rec, err := serveIncremental(t, -1, func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte("data: hi\n\n"))
|
|
})
|
|
|
|
assertRefused(t, err, rec.Header())
|
|
}
|
|
|
|
// A bodyless response has no content to forward incrementally, so there is
|
|
// nothing to refuse even with buffering configured.
|
|
func TestIncrementalProxiedWhenResponseHasNoBody(t *testing.T) {
|
|
rec, err := serveIncremental(t, -1, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want 200", rec.Code)
|
|
}
|
|
}
|
|
|
|
// The upstream response is passed through untouched when no response
|
|
// buffering is configured.
|
|
func TestIncrementalProxiedWhenResponseNotBuffered(t *testing.T) {
|
|
rec, err := serveIncremental(t, 0, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.(http.Flusher).Flush()
|
|
_, _ = w.Write([]byte("data: hi\n\n"))
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want 200", rec.Code)
|
|
}
|
|
if got := rec.Body.String(); got != "data: hi\n\n" {
|
|
t.Errorf("body = %q, want %q", got, "data: hi\n\n")
|
|
}
|
|
if got := rec.Header().Get("Incremental"); got != "?1" {
|
|
t.Errorf("downstream Incremental = %q, want ?1", got)
|
|
}
|
|
}
|
|
|
|
// Replacing an upstream response does not forward or buffer its body. The
|
|
// replacement remains valid even when that unused response was incremental.
|
|
func TestIncrementalResponseCanBeReplaced(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Incremental", "?1")
|
|
_, _ = w.Write([]byte("upstream"))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.ResponseBuffers = -1
|
|
h.HandleResponse = []caddyhttp.ResponseHandler{{}}
|
|
req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "http://example.com/", nil))
|
|
rec := httptest.NewRecorder()
|
|
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
_, writeErr := w.Write([]byte("replacement"))
|
|
return writeErr
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
if got := rec.Body.String(); got != "replacement" {
|
|
t.Errorf("body = %q, want %q", got, "replacement")
|
|
}
|
|
}
|
|
|
|
func TestIncrementalAddedByHeaderDownAfterBufferingIsRefused(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte("hello"))
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.ResponseBuffers = 4096
|
|
h.Headers = &headers.Handler{Response: &headers.RespHeaderOps{HeaderOps: &headers.HeaderOps{
|
|
Set: http.Header{"Incremental": []string{"?1"}},
|
|
}}}
|
|
req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "http://example.com/", nil))
|
|
rec := httptest.NewRecorder()
|
|
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
assertRefused(t, err, rec.Header())
|
|
}
|
|
|
|
// A software name identifies no deployment, so with nothing configured to
|
|
// identify this proxy there is no Proxy-Status field to generate: RFC 9209
|
|
// leaves generating it optional.
|
|
func TestProxyStatusOmittedWhenUnnamed(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.ProxyStatusName = ""
|
|
h.RequestBuffers = -1
|
|
|
|
rec := httptest.NewRecorder()
|
|
err := h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
|
|
var handlerErr caddyhttp.HandlerError
|
|
if !errors.As(err, &handlerErr) || handlerErr.StatusCode != http.StatusNotImplemented {
|
|
t.Fatalf("expected a 501 HandlerError, got %v", err)
|
|
}
|
|
if got, ok := rec.Header()["Proxy-Status"]; ok {
|
|
t.Errorf("Proxy-Status = %q, want no field", got)
|
|
}
|
|
}
|
|
|
|
// RFC 9209 section 2 asks intermediaries to preserve the members already in
|
|
// the field, so the whole chain that handled the response stays visible.
|
|
func TestProxyStatusPreservesUpstreamMembers(t *testing.T) {
|
|
rec, err := serveIncremental(t, -1, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Proxy-Status", "ExampleCDN")
|
|
_, _ = w.Write([]byte("data: hi\n\n"))
|
|
})
|
|
|
|
var handlerErr caddyhttp.HandlerError
|
|
if !errors.As(err, &handlerErr) || handlerErr.StatusCode != http.StatusNotImplemented {
|
|
t.Fatalf("expected a 501 HandlerError, got %v", err)
|
|
}
|
|
|
|
want := "ExampleCDN, " + testProxyStatusName + ";error=" + proxyErrorIncrementalRefused
|
|
if got := rec.Header().Get("Proxy-Status"); got != want {
|
|
t.Errorf("Proxy-Status = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestProxyStatusDropsInvalidMembers(t *testing.T) {
|
|
h := &Handler{ProxyStatusName: testProxyStatusName}
|
|
upstream := http.Header{
|
|
"Proxy-Status": []string{"?1, (inner), upstream.example"},
|
|
}
|
|
|
|
got, ok := h.proxyStatus(upstream, proxyErrorIncrementalRefused)
|
|
if !ok {
|
|
t.Fatal("proxyStatus() did not produce a value")
|
|
}
|
|
want := testProxyStatusName + ";error=" + proxyErrorIncrementalRefused
|
|
if got != want {
|
|
t.Errorf("Proxy-Status = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// A name a token cannot hold is quoted as a string instead, which RFC 9209
|
|
// allows just as well.
|
|
func TestProxyStatusNameNeedingQuotes(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.ProxyStatusName = "Example CDN"
|
|
h.RequestBuffers = -1
|
|
|
|
rec := httptest.NewRecorder()
|
|
_ = h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
|
|
want := `"Example CDN";error=` + proxyErrorIncrementalRefused
|
|
if got := rec.Header().Get("Proxy-Status"); got != want {
|
|
t.Errorf("Proxy-Status = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// HTTP/3 reports an unknown length for bodyless GET and HEAD requests. The
|
|
// transport still needs to receive these requests rather than a 501 refusal.
|
|
func TestIncrementalProxiedWhenBodylessRequestHasUnknownLength(t *testing.T) {
|
|
var reached bool
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.RequestBuffers = 4096
|
|
h.Transport = lengthRequiringTransport{testTransport{&http.Transport{}}}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "http://example.com/", io.NopCloser(strings.NewReader("")))
|
|
req.Header.Set("Incremental", "?1")
|
|
req = prepareTestRequest(req)
|
|
if req.ContentLength != -1 {
|
|
t.Fatalf("ContentLength = %d, want -1", req.ContentLength)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
if err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
})); err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
|
|
if !reached {
|
|
t.Error("bodyless request was not forwarded upstream")
|
|
}
|
|
}
|
|
|
|
func TestIncrementalAddedByHeaderUpWhenBodylessRequestHasUnknownLength(t *testing.T) {
|
|
h := incrementalHandler("unused.invalid")
|
|
h.Headers = &headers.Handler{Request: &headers.HeaderOps{
|
|
Set: http.Header{"Incremental": []string{"?1"}},
|
|
}}
|
|
h.Transport = lengthRequiringRoundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
if req.ContentLength != 0 {
|
|
t.Errorf("ContentLength = %d, want 0", req.ContentLength)
|
|
}
|
|
if req.Body != nil {
|
|
t.Errorf("Body = %T, want nil", req.Body)
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: http.NoBody,
|
|
ContentLength: 0,
|
|
Request: req,
|
|
}, nil
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "http://example.com/", io.NopCloser(strings.NewReader("")))
|
|
req = prepareTestRequest(req)
|
|
if req.ContentLength != -1 {
|
|
t.Fatalf("ContentLength = %d, want -1", req.ContentLength)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
if err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
})); err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIncrementalBodylessUnknownLengthIsNormalised(t *testing.T) {
|
|
h := Handler{Transport: lengthRequiringTransport{testTransport{&http.Transport{}}}}
|
|
req := httptest.NewRequest(http.MethodGet, "http://example.com/", io.NopCloser(strings.NewReader("")))
|
|
req.Header.Set("Incremental", "?1")
|
|
repl := caddy.NewReplacer()
|
|
req = caddyhttp.PrepareRequest(req, repl, nil, &caddyhttp.Server{})
|
|
|
|
got, err := h.prepareRequest(req, repl)
|
|
if err != nil {
|
|
t.Fatalf("prepareRequest() error = %v", err)
|
|
}
|
|
if got.ContentLength != 0 {
|
|
t.Errorf("ContentLength = %d, want 0", got.ContentLength)
|
|
}
|
|
if got.Body != nil {
|
|
t.Errorf("Body = %T, want nil", got.Body)
|
|
}
|
|
}
|
|
|
|
// A response to HEAD advertises the length the content would have had, while
|
|
// carrying no content at all, so there is nothing for a buffer to hold back.
|
|
func TestIncrementalProxiedWhenHeadResponseAdvertisesLength(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Incremental", "?1")
|
|
w.Header().Set("Content-Length", "10000")
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := incrementalHandler(backend.Listener.Addr().String())
|
|
h.ResponseBuffers = -1
|
|
|
|
req := prepareTestRequest(httptest.NewRequest(http.MethodHead, "http://example.com/", nil))
|
|
rec := httptest.NewRecorder()
|
|
if err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
})); err != nil {
|
|
t.Fatalf("ServeHTTP() error = %v", err)
|
|
}
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want 200", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestResponseHasContent(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
method string
|
|
status int
|
|
length int64
|
|
body io.ReadCloser
|
|
want bool
|
|
}{
|
|
{name: "unknown response", method: http.MethodGet, status: http.StatusOK, length: -1, body: io.NopCloser(strings.NewReader("x")), want: true},
|
|
{name: "HEAD", method: http.MethodHead, status: http.StatusOK, length: 10, body: io.NopCloser(strings.NewReader(""))},
|
|
{name: "informational", method: http.MethodGet, status: http.StatusEarlyHints, length: -1, body: io.NopCloser(strings.NewReader(""))},
|
|
{name: "no content", method: http.MethodGet, status: http.StatusNoContent, length: -1, body: io.NopCloser(strings.NewReader(""))},
|
|
{name: "reset content", method: http.MethodGet, status: http.StatusResetContent, length: -1, body: io.NopCloser(strings.NewReader(""))},
|
|
{name: "not modified", method: http.MethodGet, status: http.StatusNotModified, length: -1, body: io.NopCloser(strings.NewReader(""))},
|
|
{name: "successful CONNECT", method: http.MethodConnect, status: http.StatusOK, length: -1, body: io.NopCloser(strings.NewReader(""))},
|
|
{name: "failed CONNECT", method: http.MethodConnect, status: http.StatusBadGateway, length: -1, body: io.NopCloser(strings.NewReader("x")), want: true},
|
|
{name: "zero length", method: http.MethodGet, status: http.StatusOK, length: 0, body: io.NopCloser(strings.NewReader(""))},
|
|
{name: "NoBody", method: http.MethodGet, status: http.StatusOK, length: -1, body: http.NoBody},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(tc.method, "http://example.com/", nil)
|
|
res := &http.Response{StatusCode: tc.status, ContentLength: tc.length, Body: tc.body}
|
|
if got := responseHasContent(req, res); got != tc.want {
|
|
t.Errorf("responseHasContent() = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|