Repository navigation
internal/jsonrpc2: send error response when result fails to marshal - #1400
Merged
guglielmo-san merged 3 commits intoOct 16, 2026
Conversation
Comment on lines
716
to
735
| response, respErr := NewResponse(req.ID, result, err) | ||
|
|
||
| // The caller could theoretically reuse the request's ID as soon as we've | ||
| // sent the response, so ensure that it is removed from the incoming map | ||
| // before sending. | ||
| c.updateInFlight(func(s *inFlightState) { | ||
| delete(s.incomingByID, req.ID) | ||
| }) | ||
| if respErr == nil { | ||
| writeErr := c.write(notDone{req.ctx}, response) | ||
| if err == nil { | ||
| err = writeErr | ||
| } | ||
| } else { | ||
| err = c.internalErrorf("%#v returned a malformed result for %q: %w", from, req.Method, respErr) | ||
| // Still answer the call, so the peer isn't left waiting. Write | ||
| // failures are already recorded by c.write. | ||
| resp := &Response{ID: req.ID, Error: NewError(-32603, "internal error: result could not be encoded")} | ||
| _ = c.write(notDone{req.ctx}, resp) | ||
| } |
Contributor
There was a problem hiding this comment.
c.internalError already wraps the error with ErrInternal, so i think it can be modified with:
response, respErr := NewResponse(req.ID, result, err)
if respErr != nil {
err = c.internalErrorf("%#v returned a malformed result for %q: %w", from, req.Method, respErr)
response, _ = NewResponse(req.ID, nil, err)
}
// The caller could theoretically reuse the request's ID as soon as we've
// sent the response, so ensure that it is removed from the incoming map
// before sending.
c.updateInFlight(func(s *inFlightState) {
delete(s.incomingByID, req.ID)
})
writeErr := c.write(notDone{req.ctx}, response)
if err == nil {
err = writeErr
}
Contributor
Author
|
Good catch, thanks! I applied your suggestion: on a marshal failure we now build the error with c.internalErrorf (which already wraps ErrInternal), re-create the response from it via NewResponse, and send it through the normal write path. That removes the separate hand-built response branch. I also updated TestUnmarshalableResultSendsError to check for a -32603 error containing "malformed result" instead of the old fixed message. |
guglielmo-san
approved these changes
Oct 16, 2026
Contributor
|
@beiryu thank you for the contribution! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When a handler returned a result that
json.Marshalcould not encode (for example a+InforNaNfloat inStructuredContent), the connection recorded the write error but never answered the request. With the Streamable HTTP handler, the POST stayed open until the client timed out.processResultnow logs the internal error as before. It also writes a generic-32603response with the message "internal error: result could not be encoded" for the same request ID. The waiting HTTP request is released and the client gets a JSON-RPC error. The error text does not include the encoding failure details.Fixes #1358
Testing:
TestUnmarshalableResultSendsErrorininternal/jsonrpc2/conn_test.go. A handler returnsmath.Inf(1). The test checks that a-32603response with the same ID is written and that the internal error is reported.TestStreamableUnencodableToolResultinmcp/streamable_test.go. A tool returns+InfinStructuredContent. For bothStateless: trueandStateless: false, the test checks thatCallToolreturns a JSON-RPC internal error instead of hanging.I found that a tool result which cannot be encoded made the server hang with no answer. I wanted the client to get a clear error instead of waiting for its timeout.