jsonrpc2: wrap write error in server-closing error (#1186)
The "server is closing" error produced when a connection shuts down
because its write side failed formatted the underlying write error with
`%v`, so the cause - typically `io.EOF` when a stdio host closes its
pipe - was not part of the error chain. A caller that wants to
distinguish a clean host disconnect from a real failure via
`errors.Is(err, io.EOF)` therefore always got `false`.
This changes both `s.writeErr` wrappings to use `%w`, preserving the
write error in the error chain while keeping the same message text
("server is closing: EOF").
A regression test exercises the write-failure branch of `shuttingDown`
and asserts both `errors.Is(err, ErrServerClosing)` and `errors.Is(err,
io.EOF)`.
Fixes #1098
---------
Co-authored-by: Bo Zhang <187063395+Tethys0@users.noreply.github.com>
Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
This commit is contained in:
parent
b55bcbd610
commit
165121e53e
2 changed files with 40 additions and 2 deletions
|
|
@ -158,12 +158,12 @@ func (s *inFlightState) shuttingDown(errClosing error) error {
|
||||||
if s.readErr != nil {
|
if s.readErr != nil {
|
||||||
// If the read side of the connection is broken, we cannot read new call
|
// If the read side of the connection is broken, we cannot read new call
|
||||||
// requests, and cannot read responses to our outgoing calls.
|
// requests, and cannot read responses to our outgoing calls.
|
||||||
return fmt.Errorf("%w: %v", errClosing, s.readErr)
|
return fmt.Errorf("%w: %w", errClosing, s.readErr)
|
||||||
}
|
}
|
||||||
if s.writeErr != nil {
|
if s.writeErr != nil {
|
||||||
// If the write side of the connection is broken, we cannot write responses
|
// If the write side of the connection is broken, we cannot write responses
|
||||||
// for incoming calls, and cannot write requests for outgoing calls.
|
// for incoming calls, and cannot write requests for outgoing calls.
|
||||||
return fmt.Errorf("%w: %v", errClosing, s.writeErr)
|
return fmt.Errorf("%w: %w", errClosing, s.writeErr)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
internal/jsonrpc2/conn_test.go
Normal file
38
internal/jsonrpc2/conn_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by the license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package jsonrpc2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestShuttingDownWrapsWriteError verifies that when a connection shuts down
|
||||||
|
// because its write side failed, the returned error preserves the underlying
|
||||||
|
// write error in its chain so callers can classify it with errors.Is (for
|
||||||
|
// example, distinguishing io.EOF from a clean host disconnect versus a real
|
||||||
|
// failure).
|
||||||
|
func TestShuttingDownWrapsWriteError(t *testing.T) {
|
||||||
|
s := &inFlightState{writeErr: io.EOF}
|
||||||
|
err := s.shuttingDown(ErrServerClosing)
|
||||||
|
if !errors.Is(err, ErrServerClosing) {
|
||||||
|
t.Errorf("shuttingDown() error = %v, want it to wrap ErrServerClosing", err)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
t.Errorf("shuttingDown() error = %v, want it to wrap io.EOF", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShuttingDownWrapsReadError(t *testing.T) {
|
||||||
|
s := &inFlightState{readErr: io.EOF}
|
||||||
|
err := s.shuttingDown(ErrServerClosing)
|
||||||
|
if !errors.Is(err, ErrServerClosing) {
|
||||||
|
t.Errorf("shuttingDown() error = %v, want it to wrap ErrServerClosing", err)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
t.Errorf("shuttingDown() error = %v, want it to wrap io.EOF", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue