From 165121e53ee6c1d9b95b8485bc2b23d7b24ac3e9 Mon Sep 17 00:00:00 2001 From: Bobo Date: Thu, 27 Aug 2026 09:12:53 -0500 Subject: [PATCH] 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 --- internal/jsonrpc2/conn.go | 4 ++-- internal/jsonrpc2/conn_test.go | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 internal/jsonrpc2/conn_test.go diff --git a/internal/jsonrpc2/conn.go b/internal/jsonrpc2/conn.go index 56edb7e..5849a87 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 } diff --git a/internal/jsonrpc2/conn_test.go b/internal/jsonrpc2/conn_test.go new file mode 100644 index 0000000..afeb4c0 --- /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) + } +}