diff --git a/internal/jsonrpc2/conn.go b/internal/jsonrpc2/conn.go index 4994c63b..5a2253b1 100644 --- a/internal/jsonrpc2/conn.go +++ b/internal/jsonrpc2/conn.go @@ -158,12 +158,12 @@ func (s *inFlightState) shuttingDown(errClosing error) error { if s.readErr != nil { // If the read side of the connection is broken, we cannot read new call // 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 the write side of the connection is broken, we cannot write responses // 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 } @@ -671,7 +671,7 @@ func (c *Connection) handleAsync() { if s.writeErr != nil { // Assume that req.ctx was canceled due to s.writeErr. // TODO(#51365): use a Context API to plumb this through req.ctx. - err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr) + err = fmt.Errorf("%w: %w", ErrServerClosing, s.writeErr) } }) c.processResult("handleAsync", req, nil, err) diff --git a/internal/jsonrpc2/conn_test.go b/internal/jsonrpc2/conn_test.go new file mode 100644 index 00000000..afeb4c02 --- /dev/null +++ b/internal/jsonrpc2/conn_test.go @@ -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) + } +}