diff --git a/internal/jsonrpc2/conn.go b/internal/jsonrpc2/conn.go index 4994c63b..c0f42dfb 100644 --- a/internal/jsonrpc2/conn.go +++ b/internal/jsonrpc2/conn.go @@ -718,6 +718,9 @@ func (c *Connection) processResult(from any, req *incomingRequest, result any, e if err == nil { err = writeErr } + if writeErr != nil && c.onInternalError != nil { + c.onInternalError(fmt.Errorf("jsonrpc2: failed to write response for %q: %w", req.Method, writeErr)) + } } else { err = c.internalErrorf("%#v returned a malformed result for %q: %w", from, req.Method, respErr) } @@ -728,10 +731,6 @@ func (c *Connection) processResult(from any, req *incomingRequest, result any, e err = fmt.Errorf("%w: %q notification failed: %v", ErrInternal, req.Method, err) } } - if err != nil { - // TODO: can/should we do anything with this error beyond writing it to the event log? - // (Is this the right label to attach to the log?) - } // Cancel the request to free any associated resources. req.cancel() diff --git a/internal/jsonrpc2/conn_test.go b/internal/jsonrpc2/conn_test.go new file mode 100644 index 00000000..646d8b89 --- /dev/null +++ b/internal/jsonrpc2/conn_test.go @@ -0,0 +1,66 @@ +// Copyright 2020 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package jsonrpc2 + +import ( + "context" + "errors" + "strings" + "testing" +) + +type errWriter struct { + err error +} + +func (w errWriter) Write(context.Context, Message) error { + return w.err +} + +func TestProcessResultWriteFailureReportsInternalError(t *testing.T) { + writeErr := errors.New("write failed") + var internalErrors []error + c := &Connection{ + done: make(chan struct{}), + writer: errWriter{err: writeErr}, + onInternalError: func(err error) { + internalErrors = append(internalErrors, err) + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + call, err := NewCall(StringID("1"), "test", nil) + if err != nil { + t.Fatal(err) + } + + req := &incomingRequest{ + Request: call, + ctx: ctx, + cancel: cancel, + } + + c.updateInFlight(func(s *inFlightState) { + s.incoming = 1 + s.incomingByID = map[ID]*incomingRequest{call.ID: req} + }) + + if err := c.processResult("test", req, "ok", nil); err != nil { + t.Fatalf("processResult() = %v, want nil", err) + } + + if len(internalErrors) != 1 { + t.Fatalf("OnInternalError calls = %d, want 1", len(internalErrors)) + } + got := internalErrors[0].Error() + if !strings.Contains(got, "failed to write response") { + t.Errorf("OnInternalError = %q, want message about write failure", got) + } + if !errors.Is(internalErrors[0], writeErr) { + t.Errorf("OnInternalError error does not wrap write error: %v", internalErrors[0]) + } +}