mcp: fix error propagation to prevent session closing (#1118)
Fixes #1117
This commit is contained in:
parent
1acd66e9d7
commit
c4a4ce4543
1 changed files with 182 additions and 25 deletions
|
|
@ -300,7 +300,12 @@ var allowsessionsinstateless = mcpgodebug.Value("allowsessionsinstateless")
|
|||
|
||||
// noprotocolerrorbody is a compatibility parameter that restores the previous
|
||||
// behavior of [streamableClientConn.checkResponse]. When unset (the default),
|
||||
// the client always attempts to surface the underlying JSON-RPC error.
|
||||
// the client attempts to decode the response body of a non-2xx response as a
|
||||
// JSON-RPC error. If successful, the underlying JSON-RPC error is surfaced to
|
||||
// the caller and wrapped with [jsonrpc2.ErrRejected] so that per-call
|
||||
// rejections do not tear down the session. When set to "1", the client
|
||||
// ignores response bodies on non-2xx responses and any non-transient error
|
||||
// permanently fails the connection.
|
||||
var noprotocolerrorbody = mcpgodebug.Value("noprotocolerrorbody")
|
||||
|
||||
// disablecontenttypecheck is a compatibility parameter that allows to disable
|
||||
|
|
@ -2304,10 +2309,13 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e
|
|||
}
|
||||
|
||||
if err := c.checkResponse(ctx, requestSummary, resp); err != nil {
|
||||
if requestMethod == methodDiscover {
|
||||
if requestMethod == methodDiscover && !errors.Is(err, jsonrpc2.ErrRejected) {
|
||||
// Wrap the discover failure with ErrRejected so the jsonrpc2 layer
|
||||
// doesn't set writeErr, which would prevent the legacy initialize
|
||||
// fallback from succeeding on the same connection.
|
||||
// fallback from succeeding on the same connection. This covers the
|
||||
// case where a legacy server rejects server/discover with a
|
||||
// non-JSON-RPC body (e.g. plain text 400), which checkResponse
|
||||
// cannot classify as a per-call rejection on its own.
|
||||
err = fmt.Errorf("%w: %w", err, jsonrpc2.ErrRejected)
|
||||
} else if !errors.Is(err, jsonrpc2.ErrRejected) {
|
||||
// Only fail the connection for non-transient errors.
|
||||
|
|
@ -2528,6 +2536,21 @@ func (c *streamableClientConn) checkResponse(ctx context.Context, requestSummary
|
|||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
// Transient server errors (502, 503, 504, 429) should not break the connection.
|
||||
// Wrap them with ErrRejected so the jsonrpc2 layer doesn't set writeErr.
|
||||
if isTransientHTTPStatus(resp.StatusCode) {
|
||||
return fmt.Errorf("%w: %s: %v", jsonrpc2.ErrRejected, requestSummary, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
// By default, always try to decode the body and surface the underlying
|
||||
// JSON-RPC error, wrapping it with ErrRejected to prevent the connection from closing.
|
||||
// Setting MCPGODEBUG=noprotocolerrorbody=1 restores the previous behavior.
|
||||
if noprotocolerrorbody != "1" && (resp.StatusCode < 200 || resp.StatusCode >= 300) {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
msg, _ := jsonrpc.DecodeMessage(body)
|
||||
if response, ok := msg.(*jsonrpc.Response); ok && response.Error != nil {
|
||||
return fmt.Errorf("%s: %w: %w: %v", requestSummary, response.Error, jsonrpc2.ErrRejected, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
}
|
||||
// §2.5.3: "The server MAY terminate the session at any time, after
|
||||
// which it MUST respond to requests containing that session ID with HTTP
|
||||
// 404 Not Found."
|
||||
|
|
@ -2536,23 +2559,7 @@ func (c *streamableClientConn) checkResponse(ctx context.Context, requestSummary
|
|||
// session is already gone.
|
||||
return fmt.Errorf("%s: failed to connect (session ID: %v): %w", requestSummary, c.sessionID, ErrSessionMissing)
|
||||
}
|
||||
// Transient server errors (502, 503, 504, 429) should not break the connection.
|
||||
// Wrap them with ErrRejected so the jsonrpc2 layer doesn't set writeErr.
|
||||
if isTransientHTTPStatus(resp.StatusCode) {
|
||||
return fmt.Errorf("%w: %s: %v", jsonrpc2.ErrRejected, requestSummary, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
// By default, always try to decode the body and surface the underlying
|
||||
// JSON-RPC error.
|
||||
// Setting MCPGODEBUG=noprotocolerrorbody=1 restores the previous behavior.
|
||||
if noprotocolerrorbody == "1" {
|
||||
return fmt.Errorf("%s: %v", requestSummary, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
msg, _ := jsonrpc.DecodeMessage(body)
|
||||
if response, ok := msg.(*jsonrpc.Response); ok && response.Error != nil {
|
||||
return fmt.Errorf("%s: %w: %v", requestSummary, response.Error, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
return fmt.Errorf("%s: %v", requestSummary, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Reference in a new issue