fix: enforce retry limit when SSE stream makes no progress (#742)

Fixes #679

## Changes
- Track last event ID to detect progress in SSE streams
- Only reset retry counter when Last-Event-ID advances
- Fail connection after exceeding maxRetries without progress

This implements Option 2 from the issue: progress-based retry limiting
that prevents infinite retry loops while allowing long-running streams
to continue as long as progress is made.

---------

Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Maciej Kisiel <mkisiel@google.com>
This commit is contained in:
lif 2026-02-03 17:17:32 +08:00 • committed by GitHub
commit cf88bda0c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1823,15 +1823,14 @@ func (c *streamableClientConn) handleJSON(requestSummary string, resp *http.Resp
// stream is complete when we receive its response. Otherwise, this is the
// standalone stream.
func (c *streamableClientConn) handleSSE(ctx context.Context, requestSummary string, resp *http.Response, forCall *jsonrpc2.Request) {
// Track the last event ID to detect progress.
// The retry counter is only reset when progress is made (lastEventID advances).
// This prevents infinite retry loops when a server repeatedly terminates
// connections without making progress (#679).
var prevLastEventID string
retriesWithoutProgress := 0
for {
// Connection was successful. Continue the loop with the new response.
//
// TODO(#679): we should set a reasonable limit on the number of times
// we'll try getting a response for a given request, or enforce that we
// actually make progress.
//
// Eventually, if we don't get the response, we should stop trying and
// fail the request.
lastEventID, reconnectDelay, clientClosed := c.processStream(ctx, requestSummary, resp, forCall)
// If the connection was closed by the client, we're done.
@ -1845,6 +1844,23 @@ func (c *streamableClientConn) handleSSE(ctx context.Context, requestSummary str
return
}
// Check if we made progress (lastEventID advanced).
// Only reset the retry counter when actual progress is made.
if lastEventID != "" && lastEventID != prevLastEventID {
// Progress was made: reset the retry counter.
retriesWithoutProgress = 0
prevLastEventID = lastEventID
} else {
// No progress: increment the retry counter.
retriesWithoutProgress++
if retriesWithoutProgress > c.maxRetries {
if ctx.Err() == nil {
c.fail(fmt.Errorf("%s: exceeded %d retries without progress (session ID: %v)", requestSummary, c.maxRetries, c.sessionID))
}
return
}
}
// The stream was interrupted or ended by the server. Attempt to reconnect.
newResp, err := c.connectSSE(ctx, lastEventID, reconnectDelay, false)
if err != nil {