cloudflare/cloudflared · error
write to closed websocket connection
Error message
write to closed websocket connection
What it means
websocket/connection.go's Conn.Write guards against writing after Close was called (fix for TUN-5184). If the connection is marked done, it returns this error instead of passing bytes down to wsutil, which would otherwise panic or silently corrupt state. It signals the caller that the websocket lifecycle has ended and the write was discarded (0 bytes written).
Source
Thrown at websocket/connection.go:115
return c
}
// Read will read messages from the websocket connection
func (c *Conn) Read(reader []byte) (int, error) {
data, err := wsutil.ReadClientBinary(c.rw)
if err != nil {
return 0, err
}
return copy(reader, data), nil
}
// Write will write messages to the websocket connection.
// It will not write to the connection after Close is called to fix TUN-5184
func (c *Conn) Write(p []byte) (int, error) {
c.writeLock.Lock()
defer c.writeLock.Unlock()
if c.done {
return 0, errors.New("write to closed websocket connection")
}
if err := wsutil.WriteServerBinary(c.rw, p); err != nil {
return 0, err
}
return len(p), nil
}
func (c *Conn) pinger(ctx context.Context) {
pongMessge := wsutil.Message{
OpCode: gobwas.OpPong,
Payload: []byte{},
}
ticker := time.NewTicker(c.pingPeriod(ctx))
defer ticker.Stop()
for {
select {View on GitHub (pinned to 2253eeeb25)
Solutions
- Check connection state before writing: stop the write loop when Close/done is signaled (e.g. select on a done channel).
- Treat this error as a benign shutdown signal — log at debug and break out of the write loop rather than retrying.
- Serialize lifecycle: ensure Close() is only called after all in-flight writers have returned, or guard writes with the same done check.
- If writes regularly race with close, restructure so the owning goroutine closes the connection last.
Example fix
// before: blind write
n, err := wsConn.Write(data)
// after: tolerate closed-connection teardown
n, err := wsConn.Write(data)
if err != nil {
if err.Error() == "write to closed websocket connection" {
return nil // connection already torn down
}
return err
} Defensive patterns
Strategy: type-guard
Validate before calling
// guard: only write while the connection is open
if wsConn == nil || wsConn.IsClosed() {
return errors.New("websocket not open; skipping write")
} Type guard
func writable(c *websocket.Conn) bool {
return c != nil && !c.IsClosed()
} Try / catch
n, err := wsConn.Write(p)
if err != nil {
if strings.Contains(err.Error(), "write to closed websocket connection") {
return 0, nil // expected during shutdown
}
return n, err
} Prevention
- Stop writer goroutines via a done channel before calling Close()
- Never fan out writes to a Conn from goroutines that outlive the session
- Treat closed-connection write errors as normal teardown, not failures
- Ensure the reader loop exits trigger Close only after writers are drained
When it happens
Trigger: Any caller of (*websocket.Conn).Write — e.g. echoTCPOrigin, WriteEvent, echoTCP — invoking Write after another goroutine has called Close() on the same Conn (c.done set to true under writeLock).
Common situations: A proxied TCP/SSH session where the client disconnected and the origin loop still tries to flush buffered data; a stream/heartbeat (WriteEvent) racing with connection teardown during shutdown; writing to a websocket whose dial failed or whose reader already exited.
Related errors
- internal error: unsupported connection type
- status not yet written before attempting to hijack connectio
- cloudflared already shutdown
- error setting read deadline: %w
- error setting write deadline: %w
AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06).
Data as JSON: /api/errors/85e8e76e2eb942ac.
Report an issue: GitHub.