Skip to content

Commit e5aa423

Browse files
committed
fix: suppress net.ErrClosed on concurrent Close
Close and CloseNow returned a wrapped net.ErrClosed when the internal read loop or write path won the casClosing race before the user-facing call. The defer that suppresses net.ErrClosed was only registered on the CAS winner path. Separate user-facing close tracking from internal close coordination. A userClosed atomic tracks whether the user has called Close/CloseNow before. Combined with isClosed, this gives three distinct behaviors: - First user call, close in progress or peer closed: nil. - Concurrent user calls while close is in progress: nil, returns instantly without blocking. - User call after close has fully completed: net.ErrClosed.
1 parent d099e16 commit e5aa423

2 files changed

Lines changed: 14 additions & 13 deletions

File tree

close.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,7 @@ func CloseStatus(err error) StatusCode {
9999
func (c *Conn) Close(code StatusCode, reason string) (err error) {
100100
defer errd.Wrap(&err, "failed to close WebSocket")
101101

102-
if c.casClosing() {
103-
err = c.waitGoroutines()
104-
if err != nil {
105-
return err
106-
}
102+
if c.userClosed.Swap(true) && c.isClosed() {
107103
return net.ErrClosed
108104
}
109105
defer func() {
@@ -112,6 +108,10 @@ func (c *Conn) Close(code StatusCode, reason string) (err error) {
112108
}
113109
}()
114110

111+
if c.casClosing() {
112+
return c.waitGoroutines()
113+
}
114+
115115
err = c.closeHandshake(code, reason)
116116

117117
err2 := c.close()
@@ -132,11 +132,7 @@ func (c *Conn) Close(code StatusCode, reason string) (err error) {
132132
func (c *Conn) CloseNow() (err error) {
133133
defer errd.Wrap(&err, "failed to immediately close WebSocket")
134134

135-
if c.casClosing() {
136-
err = c.waitGoroutines()
137-
if err != nil {
138-
return err
139-
}
135+
if c.userClosed.Swap(true) && c.isClosed() {
140136
return net.ErrClosed
141137
}
142138
defer func() {
@@ -145,6 +141,10 @@ func (c *Conn) CloseNow() (err error) {
145141
}
146142
}()
147143

144+
if c.casClosing() {
145+
return c.waitGoroutines()
146+
}
147+
148148
err = c.close()
149149

150150
err2 := c.waitGoroutines()

conn.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ type Conn struct {
7777
closeReadCtx context.Context
7878
closeReadDone chan struct{}
7979

80-
closing atomic.Bool
81-
closeMu sync.Mutex // Protects following.
82-
closed chan struct{}
80+
userClosed atomic.Bool // Set by Close/CloseNow on first user call.
81+
closing atomic.Bool
82+
closeMu sync.Mutex // Protects following.
83+
closed chan struct{}
8384

8485
pingCounter atomic.Int64
8586
activePingsMu sync.Mutex

0 commit comments

Comments
 (0)