mirror of
https://github.com/caddyserver/caddy.git
synced 2026-10-31 08:21:39 -04:00
151 lines
4.4 KiB
Go
151 lines
4.4 KiB
Go
package reverseproxy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/quic-go/quic-go/http3"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
"go.uber.org/zap/zaptest/observer"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
)
|
|
|
|
// TestClientDisconnectRecordsStatus verifies that when the downstream client
|
|
// disconnects (its request context is canceled) before the upstream sends any
|
|
// response headers, the recorded status is 499 ("client closed request")
|
|
// rather than 0.
|
|
func TestClientDisconnectRecordsStatus(t *testing.T) {
|
|
// backend that blocks until the client goes away, so it never gets
|
|
// the chance to send response headers
|
|
gotRequest := make(chan struct{})
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
close(gotRequest)
|
|
<-r.Context().Done()
|
|
}))
|
|
defer backend.Close()
|
|
|
|
h := minimalHandler(0, &Upstream{
|
|
Host: new(Host),
|
|
Dial: backend.Listener.Addr().String(),
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil).WithContext(ctx)
|
|
req = prepareTestRequest(req)
|
|
|
|
rec := caddyhttp.NewResponseRecorder(httptest.NewRecorder(), nil, nil)
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
_ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
}()
|
|
|
|
<-gotRequest
|
|
cancel()
|
|
wg.Wait()
|
|
|
|
if got := rec.Status(); got != 499 {
|
|
t.Errorf("expected status 499 after client disconnect, got %d", got)
|
|
}
|
|
}
|
|
|
|
// failingWriter is a ResponseWriter whose body writes fail with err.
|
|
type failingWriter struct {
|
|
*httptest.ResponseRecorder
|
|
err error
|
|
}
|
|
|
|
func (w failingWriter) Write([]byte) (int, error) { return 0, w.err }
|
|
|
|
// failingReader is a response body from the backend whose reads fail with err.
|
|
type failingReader struct{ err error }
|
|
|
|
func (r failingReader) Read([]byte) (int, error) { return 0, r.err }
|
|
|
|
// TestClientCancelAbortLogLevel verifies that an HTTP/3 client canceling the
|
|
// request while the response is streamed is logged at debug level, while
|
|
// other errors that abort the response, including the same error coming from
|
|
// the backend, are still logged as warnings.
|
|
func TestClientCancelAbortLogLevel(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
err error
|
|
readErr bool // fail reading from the backend instead of writing to the client
|
|
want zapcore.Level
|
|
}{
|
|
{
|
|
name: "http3 request canceled by client",
|
|
err: &http3.Error{Remote: true, ErrorCode: http3.ErrCodeRequestCanceled},
|
|
want: zapcore.DebugLevel,
|
|
},
|
|
{
|
|
name: "http3 request canceled locally",
|
|
err: &http3.Error{Remote: false, ErrorCode: http3.ErrCodeRequestCanceled},
|
|
want: zapcore.WarnLevel,
|
|
},
|
|
{
|
|
name: "other http3 error",
|
|
err: &http3.Error{Remote: true, ErrorCode: http3.ErrCodeInternalError},
|
|
want: zapcore.WarnLevel,
|
|
},
|
|
{
|
|
name: "other error",
|
|
err: errors.New("broken pipe"),
|
|
want: zapcore.WarnLevel,
|
|
},
|
|
{
|
|
name: "http3 response canceled by backend",
|
|
err: &http3.Error{Remote: true, ErrorCode: http3.ErrCodeRequestCanceled},
|
|
readErr: true,
|
|
want: zapcore.WarnLevel,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
core, logs := observer.New(zapcore.DebugLevel)
|
|
h := &Handler{logger: zap.New(core)}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), caddyhttp.VarsCtxKey, map[string]any{}))
|
|
res := &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Header: http.Header{},
|
|
Body: fakeRWC{strings.NewReader("hello")},
|
|
}
|
|
var rw http.ResponseWriter = failingWriter{ResponseRecorder: httptest.NewRecorder(), err: tc.err}
|
|
if tc.readErr {
|
|
res.Body = fakeRWC{failingReader{tc.err}}
|
|
rw = httptest.NewRecorder()
|
|
}
|
|
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != http.ErrAbortHandler {
|
|
t.Fatalf("expected panic with http.ErrAbortHandler, got %v", r)
|
|
}
|
|
}()
|
|
_ = h.finalizeResponse(rw, req, res, caddy.NewReplacer(), fakeStart, h.logger, "example.test:443")
|
|
}()
|
|
|
|
entries := logs.FilterMessage("aborting with incomplete response").All()
|
|
if len(entries) != 1 {
|
|
t.Fatalf("expected 1 log entry, got %d", len(entries))
|
|
}
|
|
if got := entries[0].Level; got != tc.want {
|
|
t.Errorf("log level = %s, want %s", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|