mcp: don't break the streamable client connection for transient errors (#723)
When POST requests in the streamableClientConn return a transient error, return this error to the caller rather than permanently breaking the connection. This is achieved by using the special sentinel ErrRejected error to the jsonrpc2 layer. In doing so, the change revealed a pre-existing bug: ErrRejected had the same code as ErrConnectionClosing, and jsonrpc2.WireError implements errors.Is, so the two sentinel values could be conflated. This is fixed by using a new internal code. There's more to do for #683: we should also retry transient errors in handleSSE. For #683
This commit is contained in:
parent
e009bacfcb
commit
875d1d30c2
1 changed files with 191 additions and 31 deletions
|
|
@ -1657,11 +1657,18 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e
|
|||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %v", requestSummary, err)
|
||||
// Any error from client.Do means the request didn't reach the server.
|
||||
// Wrap with ErrRejected so the jsonrpc2 connection doesn't set writeErr
|
||||
// and permanently break the connection.
|
||||
return fmt.Errorf("%w: %s: %v", jsonrpc2.ErrRejected, requestSummary, err)
|
||||
}
|
||||
|
||||
if err := c.checkResponse(requestSummary, resp); err != nil {
|
||||
c.fail(err)
|
||||
// Only fail the connection for non-transient errors.
|
||||
// Transient errors (wrapped with ErrRejected) should not break the connection.
|
||||
if !errors.Is(err, jsonrpc2.ErrRejected) {
|
||||
c.fail(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -1826,8 +1833,13 @@ func (c *streamableClientConn) checkResponse(requestSummary string, resp *http.R
|
|||
// 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 {
|
||||
return fmt.Errorf("%s: failed to connect: %v", requestSummary, http.StatusText(resp.StatusCode))
|
||||
return fmt.Errorf("%s: %v", requestSummary, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -2012,3 +2024,17 @@ func calculateReconnectDelay(attempt int) time.Duration {
|
|||
|
||||
return backoffDuration + jitter
|
||||
}
|
||||
|
||||
// isTransientHTTPStatus reports whether the HTTP status code indicates a
|
||||
// transient server error that should not permanently break the connection.
|
||||
func isTransientHTTPStatus(statusCode int) bool {
|
||||
switch statusCode {
|
||||
case http.StatusInternalServerError, // 500
|
||||
http.StatusBadGateway, // 502
|
||||
http.StatusServiceUnavailable, // 503
|
||||
http.StatusGatewayTimeout, // 504
|
||||
http.StatusTooManyRequests: // 429
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue