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
+}