report read limit violations on EventDisconnected via ErrReadLimit sentinel

This commit is contained in:
Jay
2026-08-10 16:02:57 -04:00
parent 19810798a7
commit 7851d22c22
4 changed files with 91 additions and 6 deletions
+8 -5
View File
@@ -19,6 +19,7 @@ import (
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
var ErrConnectionUnavailable = errors.New("connection unavailable") var ErrConnectionUnavailable = errors.New("connection unavailable")
var ErrReadLimit = transport.ErrReadLimit
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Config // Config
@@ -249,6 +250,8 @@ func (w *session) serve(
stopKeepalive, inactive, heartbeat := w.startKeepalive() stopKeepalive, inactive, heartbeat := w.startKeepalive()
// run session loop // run session loop
var disconnectErr error
session: session:
for { for {
select { select {
@@ -257,14 +260,13 @@ session:
case data, ok := <-conn.Incoming(): case data, ok := <-conn.Incoming():
if !ok { if !ok {
var reason error
select { select {
case reason = <-conn.Errors(): case disconnectErr = <-conn.Errors():
default: default:
reason = fmt.Errorf("unknown") disconnectErr = fmt.Errorf("unknown")
} }
if w.logger != nil { if w.logger != nil {
w.logger.Info("websocket: closed", "reason", reason) w.logger.Info("websocket: closed", "reason", disconnectErr)
} }
break session break session
} }
@@ -297,7 +299,8 @@ session:
// tear down connection // tear down connection
w.conn.Store(nil) w.conn.Store(nil)
pool.Events <- PoolEvent{URL: w.url, Kind: EventDisconnected, At: time.Now()} pool.Events <- PoolEvent{
URL: w.url, Kind: EventDisconnected, Err: disconnectErr, At: time.Now()}
if w.logger != nil { if w.logger != nil {
w.logger.Info("disconnected") w.logger.Info("disconnected")
+31
View File
@@ -80,6 +80,37 @@ func TestSession(t *testing.T) {
}, "expected EventConnected") }, "expected EventConnected")
}) })
t.Run("EventDisconnected carries ErrReadLimit when read limit exceeded", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mockSocket := honeybeetest.NewMockSocket()
mockSocket.ReadMessageFunc = func() (int, []byte, error) {
return 0, nil, websocket.ErrReadLimit
}
config, _ := NewSessionConfig(
WithRetryDisabled(), WithReconnectDelay(0))
w := makeSession(t, mockSocket, config, ctx, cancel)
_, events, pool := makeSessionContext(t)
var wg sync.WaitGroup
wg.Go(func() {
w.Start(pool)
})
honeybeetest.Eventually(t, func() bool {
select {
case e := <-events:
return e.Kind == EventDisconnected &&
e.Err != nil &&
errors.Is(e.Err, ErrReadLimit)
default:
return false
}
}, "expected EventDisconnected with Err matching ErrReadLimit")
})
t.Run("dial failure exhausted - session exits cleanly, no connected/disconnected events", func(t *testing.T) { t.Run("dial failure exhausted - session exits cleanly, no connected/disconnected events", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
+7 -1
View File
@@ -27,6 +27,7 @@ var (
ErrPeerClosedClean = errors.New("peer closed connection cleanly") ErrPeerClosedClean = errors.New("peer closed connection cleanly")
ErrPeerClosedUnexpected = errors.New("peer closed connection unexpectedly") ErrPeerClosedUnexpected = errors.New("peer closed connection unexpectedly")
ErrReadError = errors.New("read error") ErrReadError = errors.New("read error")
ErrReadLimit = errors.New("read limit exceeded")
) )
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -350,7 +351,12 @@ func (c *Connection) classifyCloseError(err error) error {
} }
} }
classifiedError = fmt.Errorf("%w: %w", ErrReadError, err) if errors.Is(err, websocket.ErrReadLimit) {
classifiedError = fmt.Errorf("%w: %w: %v",
ErrReadError, ErrReadLimit, err)
} else {
classifiedError = fmt.Errorf("%w: %w", ErrReadError, err)
}
} }
return classifiedError return classifiedError
+45
View File
@@ -225,6 +225,51 @@ func TestConnection_Errors(t *testing.T) {
}, "expected read error") }, "expected read error")
}) })
t.Run("read limit exceeded", func(t *testing.T) {
mockSocket := honeybeetest.NewMockSocket()
mockSocket.ReadMessageFunc = func() (int, []byte, error) {
return 0, nil, websocket.ErrReadLimit
}
conn, err := NewConnection(context.Background(), mockSocket, nil, nil)
assert.NoError(t, err)
defer conn.Close()
honeybeetest.Eventually(t, func() bool {
select {
case err := <-conn.Errors():
return errors.Is(err, ErrReadLimit) &&
errors.Is(err, ErrReadError) &&
!errors.Is(err, websocket.ErrReadLimit)
default:
return false
}
}, "expected read limit error classified as ErrReadLimit")
})
t.Run("generic read error is not a read limit error", func(t *testing.T) {
mockSocket := honeybeetest.NewMockSocket()
mockSocket.ReadMessageFunc = func() (int, []byte, error) {
return 0, nil, io.EOF
}
conn, err := NewConnection(context.Background(), mockSocket, nil, nil)
assert.NoError(t, err)
defer conn.Close()
honeybeetest.Eventually(t, func() bool {
select {
case err := <-conn.Errors():
return errors.Is(err, ErrReadError) &&
!errors.Is(err, ErrReadLimit)
default:
return false
}
}, "expected generic read error without ErrReadLimit")
})
} }
func TestConnection_Heartbeat(t *testing.T) { func TestConnection_Heartbeat(t *testing.T) {