From 483973263815b9c15ee40e00381a722d550823d1 Mon Sep 17 00:00:00 2001 From: akshay Date: Sun, 19 Jul 2026 14:17:06 +0530 Subject: [PATCH] internal/jsonrpc2: wrap writeErr with %w so errors.Is works for consumers Fixes #1098. When a connection shuts down due to a write failure, the error was formatted with %v for s.writeErr, which meant the underlying error (typically io.EOF) was not in the error chain. Consumers could not use errors.Is(err, io.EOF) to distinguish a clean host disconnect from a real failure. Change %v to %w so that both ErrServerClosing and the write error are properly wrapped and discoverable via errors.Is/errors.As. --- internal/jsonrpc2/conn.go | 2 +- internal/jsonrpc2/wire_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/jsonrpc2/conn.go b/internal/jsonrpc2/conn.go index 4994c63b..bde92baa 100644 --- a/internal/jsonrpc2/conn.go +++ b/internal/jsonrpc2/conn.go @@ -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/wire_test.go b/internal/jsonrpc2/wire_test.go index cf7e2b86..e160d6de 100644 --- a/internal/jsonrpc2/wire_test.go +++ b/internal/jsonrpc2/wire_test.go @@ -7,6 +7,9 @@ package jsonrpc2_test import ( "bytes" "encoding/json" + "errors" + "fmt" + "io" "reflect" "testing" @@ -147,6 +150,22 @@ func TestDecodeIDOnlyMessageIsResponse(t *testing.T) { } } +// TestServerClosingErrorWrapsWriteErr verifies that the error produced when a +// connection shuts down due to a write failure wraps both ErrServerClosing and +// the underlying write error, so that errors.Is works for both sentinels. +// Regression test for https://github.com/modelcontextprotocol/go-sdk/issues/1098. +func TestServerClosingErrorWrapsWriteErr(t *testing.T) { + writeErr := io.EOF + err := fmt.Errorf("%w: %w", jsonrpc2.ErrServerClosing, writeErr) + + if !errors.Is(err, jsonrpc2.ErrServerClosing) { + t.Error("errors.Is(err, ErrServerClosing) = false, want true") + } + if !errors.Is(err, io.EOF) { + t.Error("errors.Is(err, io.EOF) = false, want true") + } +} + func checkJSON(t *testing.T, got, want []byte) { // compare the compact form, to allow for formatting differences g := &bytes.Buffer{}