@marcel91200 / caddy Mirror · read-only / commits / 0bbda5c7
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, 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