@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 150 lines 4.38 KB
Raw
1 package reverseproxy
2
3 import (
4 "context"
5 "errors"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "sync"
10 "testing"
11
12 "github.com/quic-go/quic-go/http3"
13 "go.uber.org/zap"
14 "go.uber.org/zap/zapcore"
15 "go.uber.org/zap/zaptest/observer"
16
17 "github.com/caddyserver/caddy/v2"
18 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
19 )
20
21 // TestClientDisconnectRecordsStatus verifies that when the downstream client
22 // disconnects (its request context is canceled) before the upstream sends any
23 // response headers, the recorded status is 499 ("client closed request")
24 // rather than 0.
25 func TestClientDisconnectRecordsStatus(t *testing.T) {
26 // backend that blocks until the client goes away, so it never gets
27 // the chance to send response headers
28 gotRequest := make(chan struct{})
29 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30 close(gotRequest)
31 <-r.Context().Done()
32 }))
33 defer backend.Close()
34
35 h := minimalHandler(0, &Upstream{
36 Host: new(Host),
37 Dial: backend.Listener.Addr().String(),
38 })
39
40 ctx, cancel := context.WithCancel(context.Background())
41 req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil).WithContext(ctx)
42 req = prepareTestRequest(req)
43
44 rec := caddyhttp.NewResponseRecorder(httptest.NewRecorder(), nil, nil)
45
46 var wg sync.WaitGroup
47 wg.Add(1)
48 go func() {
49 defer wg.Done()
50 _ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
51 return nil
52 }))
53 }()
54
55 <-gotRequest
56 cancel()
57 wg.Wait()
58
59 if got := rec.Status(); got != 499 {
60 t.Errorf("expected status 499 after client disconnect, got %d", got)
61 }
62 }
63
64 // failingWriter is a ResponseWriter whose body writes fail with err.
65 type failingWriter struct {
66 *httptest.ResponseRecorder
67 err error
68 }
69
70 func (w failingWriter) Write([]byte) (int, error) { return 0, w.err }
71
72 // failingReader is a response body from the backend whose reads fail with err.
73 type failingReader struct{ err error }
74
75 func (r failingReader) Read([]byte) (int, error) { return 0, r.err }
76
77 // TestClientCancelAbortLogLevel verifies that an HTTP/3 client canceling the
78 // request while the response is streamed is logged at debug level, while
79 // other errors that abort the response, including the same error coming from
80 // the backend, are still logged as warnings.
81 func TestClientCancelAbortLogLevel(t *testing.T) {
82 for _, tc := range []struct {
83 name string
84 err error
85 readErr bool // fail reading from the backend instead of writing to the client
86 want zapcore.Level
87 }{
88 {
89 name: "http3 request canceled by client",
90 err: &http3.Error{Remote: true, ErrorCode: http3.ErrCodeRequestCanceled},
91 want: zapcore.DebugLevel,
92 },
93 {
94 name: "http3 request canceled locally",
95 err: &http3.Error{Remote: false, ErrorCode: http3.ErrCodeRequestCanceled},
96 want: zapcore.WarnLevel,
97 },
98 {
99 name: "other http3 error",
100 err: &http3.Error{Remote: true, ErrorCode: http3.ErrCodeInternalError},
101 want: zapcore.WarnLevel,
102 },
103 {
104 name: "other error",
105 err: errors.New("broken pipe"),
106 want: zapcore.WarnLevel,
107 },
108 {
109 name: "http3 response canceled by backend",
110 err: &http3.Error{Remote: true, ErrorCode: http3.ErrCodeRequestCanceled},
111 readErr: true,
112 want: zapcore.WarnLevel,
113 },
114 } {
115 t.Run(tc.name, func(t *testing.T) {
116 core, logs := observer.New(zapcore.DebugLevel)
117 h := &Handler{logger: zap.New(core)}
118
119 req := httptest.NewRequest(http.MethodGet, "/", nil)
120 req = req.WithContext(context.WithValue(req.Context(), caddyhttp.VarsCtxKey, map[string]any{}))
121 res := &http.Response{
122 StatusCode: http.StatusOK,
123 Header: http.Header{},
124 Body: fakeRWC{strings.NewReader("hello")},
125 }
126 var rw http.ResponseWriter = failingWriter{ResponseRecorder: httptest.NewRecorder(), err: tc.err}
127 if tc.readErr {
128 res.Body = fakeRWC{failingReader{tc.err}}
129 rw = httptest.NewRecorder()
130 }
131
132 func() {
133 defer func() {
134 if r := recover(); r != http.ErrAbortHandler {
135 t.Fatalf("expected panic with http.ErrAbortHandler, got %v", r)
136 }
137 }()
138 _ = h.finalizeResponse(rw, req, res, caddy.NewReplacer(), fakeStart, h.logger, "example.test:443")
139 }()
140
141 entries := logs.FilterMessage("aborting with incomplete response").All()
142 if len(entries) != 1 {
143 t.Fatalf("expected 1 log entry, got %d", len(entries))
144 }
145 if got := entries[0].Level; got != tc.want {
146 t.Errorf("log level = %s, want %s", got, tc.want)
147 }
148 })
149 }
150 }