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, 07:15 UTC
fix: close resources on error paths (#7940)
yangshenghui committed
Aug 16, 2026 at 19:02 UTC
0bbda5c728e7694ba929aa4fa0c549f1387d3a3f
6 files changed
+141
-1
caddytest/caddytest.go
+2
@@ -279,9 +279,11 @@ func validateTestPrerequisites(tc *Tester) error {
279
return err
280
}
281
tc.t.Cleanup(func() {
282
+ _ = f.Close()
283
os.Remove(f.Name()) //nolint:gosec // false positive, filename comes from std lib, no path traversal
284
})
285
if _, err := fmt.Fprintf(f, initConfig, tc.config.AdminPort); err != nil {
286
+ _ = f.Close()
287
return err
288
}
289
cmd/packagesfuncs.go
+1
@@ -288,6 +288,7 @@ func downloadBuild(qs url.Values) (*http.Response, error) {
288
return nil, fmt.Errorf("secure request failed: %v", err)
289
}
290
if resp.StatusCode >= 400 {
291
+ defer resp.Body.Close()
292
var details struct {
293
StatusCode int `json:"status_code"`
294
Error struct {
cmd/packagesfuncs_test.go
new
+48
@@ -0,0 +1,48 @@
1
+package caddycmd
2
+
3
+import (
4
+ "bytes"
5
+ "io"
6
+ "net/http"
7
+ "net/url"
8
+ "testing"
9
+)
10
+
11
+type trackingReadCloser struct {
12
+ *bytes.Reader
13
+ closed bool
14
+}
15
+
16
+func (r *trackingReadCloser) Close() error {
17
+ r.closed = true
18
+ return nil
19
+}
20
+
21
+func TestDownloadBuildClosesErrorResponseBody(t *testing.T) {
22
+ originalTransport := http.DefaultTransport
23
+ t.Cleanup(func() { http.DefaultTransport = originalTransport })
24
+
25
+ body := &trackingReadCloser{Reader: bytes.NewReader([]byte(`{"error":{"message":"bad request","id":"test"}}`))}
26
+ http.DefaultTransport = roundTripperFunc(func(req *http.Request) (*http.Response, error) {
27
+ return &http.Response{
28
+ StatusCode: http.StatusBadRequest,
29
+ Body: body,
30
+ Request: req,
31
+ }, nil
32
+ })
33
+
34
+ if _, err := downloadBuild(url.Values{}); err == nil {
35
+ t.Fatal("downloadBuild succeeded, want error")
36
+ }
37
+ if !body.closed {
38
+ t.Fatal("downloadBuild did not close the error response body")
39
+ }
40
+}
41
+
42
+type roundTripperFunc func(*http.Request) (*http.Response, error)
43
+
44
+func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
45
+ return f(req)
46
+}
47
+
48
+var _ io.ReadCloser = (*trackingReadCloser)(nil)
modules/caddyhttp/reverseproxy/httptransport.go
+7
@@ -296,6 +296,12 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
296
// decide whether to retry a request
297
return nil, DialError{err}
298
}
299
+ keepConn := false
300
+ defer func() {
301
+ if !keepConn {
302
+ _ = conn.Close()
303
+ }
304
+ }()
305
306
if h.ProxyProtocol != "" {
307
proxyProtocolInfo, ok := caddyhttp.GetVar(ctx, proxyProtocolInfoVarKey).(ProxyProtocolInfo)
@@ -363,6 +369,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
369
}
370
}
371
372
+ keepConn = true
373
return conn, nil
374
}
375
modules/caddyhttp/reverseproxy/httptransport_test.go
+74
@@ -3,11 +3,15 @@ package reverseproxy
3
import (
4
"context"
5
"encoding/json"
6
+ "errors"
7
"fmt"
8
+ "io"
9
"net"
10
+ "net/netip"
11
"net/url"
12
"reflect"
13
"testing"
14
+ "time"
15
16
"github.com/caddyserver/caddy/v2"
17
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -198,6 +202,76 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
202
}
203
}
204
205
+func TestHTTPTransport_DialContext_ProxyProtocolClosesConnectionOnError(t *testing.T) {
206
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
207
+ defer cancel()
208
+
209
+ validAddrPort := netip.MustParseAddrPort("192.0.2.1:1234")
210
+ tests := []struct {
211
+ name string
212
+ version string
213
+ proxyInfo *ProxyProtocolInfo
214
+ }{
215
+ {
216
+ name: "missing proxy protocol info",
217
+ version: "v1",
218
+ },
219
+ {
220
+ name: "unexpected proxy protocol version",
221
+ version: "v3",
222
+ proxyInfo: &ProxyProtocolInfo{AddrPort: validAddrPort},
223
+ },
224
+ {
225
+ name: "unexpected remote address type",
226
+ version: "v1",
227
+ proxyInfo: &ProxyProtocolInfo{},
228
+ },
229
+ }
230
+
231
+ for _, tt := range tests {
232
+ t.Run(tt.name, func(t *testing.T) {
233
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
234
+ if err != nil {
235
+ t.Fatalf("listen: %v", err)
236
+ }
237
+ t.Cleanup(func() { listener.Close() })
238
+
239
+ readResult := make(chan error, 1)
240
+ go func() {
241
+ conn, err := listener.Accept()
242
+ if err != nil {
243
+ readResult <- err
244
+ return
245
+ }
246
+ defer conn.Close()
247
+ _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
248
+ var buf [1]byte
249
+ _, err = conn.Read(buf[:])
250
+ readResult <- err
251
+ }()
252
+
253
+ dialCtx := context.WithValue(context.Background(), caddyhttp.VarsCtxKey, make(map[string]any))
254
+ if tt.proxyInfo != nil {
255
+ caddyhttp.SetVar(dialCtx, proxyProtocolInfoVarKey, *tt.proxyInfo)
256
+ }
257
+
258
+ rt, err := (&HTTPTransport{ProxyProtocol: tt.version}).NewTransport(ctx)
259
+ if err != nil {
260
+ t.Fatalf("NewTransport: %v", err)
261
+ }
262
+ conn, err := rt.DialContext(dialCtx, "tcp", listener.Addr().String())
263
+ if err == nil {
264
+ conn.Close()
265
+ t.Fatal("DialContext succeeded, want error")
266
+ }
267
+
268
+ if readErr := <-readResult; !errors.Is(readErr, io.EOF) {
269
+ t.Fatalf("server read error = %v, want EOF after client connection close", readErr)
270
+ }
271
+ })
272
+ }
273
+}
274
+
275
// TestHTTPTransport_DialContext_DialInfoOverride is a regression test for
276
// issue #6447: a `tcp4/`-prefixed upstream silently fell back to plain `tcp`
277
// because dialContext only honored DialInfo for unix networks. PR #7300 widened
modules/logging/filewriter.go
+9
-1
@@ -224,9 +224,16 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
224
if err != nil {
225
return nil, err
226
}
227
+ closeFile := true
228
+ defer func() {
229
+ if closeFile {
230
+ _ = file.Close()
231
+ }
232
+ }()
233
info, err := file.Stat()
234
if roll {
229
- file.Close() // timberjack will reopen it on its own
235
+ _ = file.Close() // timberjack will reopen it on its own
236
+ closeFile = false
237
}
238
239
// Ensure already existing files have the right mode, since OpenFile will not set the mode in such case.
@@ -244,6 +251,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
251
252
// if not rolling, then the plain file handle is all we need
253
if !roll {
254
+ closeFile = false
255
return file, nil
256
}
257