@marcel91200 / caddy Mirror · read-only / commits / e05f57dc
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, 06:00 UTC

listen: don't wedge a reloaded listener sharing a socket on Windows (#7999)

Alexandre Daubois committed Sep 7, 2026 at 18:24 UTC e05f57dc7a324f7ecd6e8e8b72b944672920d495
2 files changed +91
listen.go
+4
@@ -162,6 +162,10 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
162 if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
163 return nil, fakeClosedErr(fcl)
164 }
165 + } else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
166 + // still open: this timeout is a past deadline a sibling set in Close(),
167 + // clear it so the server loop resumes instead of spinning on it
168 + _ = fcl.sharedListener.clearDeadline()
169 }
170
171 return nil, err
listen_test.go new
+87
@@ -0,0 +1,87 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +//go:build !unix || solaris
16 +
17 +package caddy
18 +
19 +import (
20 + "context"
21 + "errors"
22 + "net"
23 + "testing"
24 + "time"
25 +)
26 +
27 +// TestFakeCloseListenerSiblingSurvivesClose pins that closing one fakeCloseListener
28 +// does not wedge another sharing the same socket, as happens during a config reload.
29 +func TestFakeCloseListenerSiblingSurvivesClose(t *testing.T) {
30 + const network, address = "tcp", "127.0.0.1:0"
31 + lnKey := "test|" + network + "/" + address
32 +
33 + ctx := context.Background()
34 +
35 + first, err := listenReusable(ctx, lnKey, network, address, net.ListenConfig{})
36 + if err != nil {
37 + t.Fatalf("first listen: %v", err)
38 + }
39 + oldServer := first.(*fakeCloseListener)
40 +
41 + second, err := listenReusable(ctx, lnKey, network, address, net.ListenConfig{})
42 + if err != nil {
43 + t.Fatalf("second listen: %v", err)
44 + }
45 + newServer := second.(*fakeCloseListener)
46 + defer newServer.Close()
47 +
48 + if err := oldServer.Close(); err != nil {
49 + t.Fatalf("close old server: %v", err)
50 + }
51 +
52 + addr := newServer.Addr().String()
53 +
54 + // Emulate the net/http accept loop, which retries timeouts as temporary.
55 + accepted := make(chan net.Conn, 1)
56 + fatal := make(chan error, 1)
57 + go func() {
58 + for {
59 + conn, err := newServer.Accept()
60 + if err == nil {
61 + accepted <- conn
62 + return
63 + }
64 + var netErr net.Error
65 + if errors.As(err, &netErr) && netErr.Timeout() {
66 + continue
67 + }
68 + fatal <- err
69 + return
70 + }
71 + }()
72 +
73 + dialed, err := net.DialTimeout(network, addr, 2*time.Second)
74 + if err != nil {
75 + t.Fatalf("dial surviving listener: %v", err)
76 + }
77 + defer dialed.Close()
78 +
79 + select {
80 + case conn := <-accepted:
81 + conn.Close()
82 + case err := <-fatal:
83 + t.Fatalf("surviving listener returned a fatal accept error: %v", err)
84 + case <-time.After(5 * time.Second):
85 + t.Fatal("surviving listener never accepted a connection after its sibling was closed")
86 + }
87 +}