Skip to content

fix: respect reconnect=false and clamp server-supplied retry: 0#135

Merged
kinyoklion merged 1 commit into
mainfrom
rlamb/sdk-2347/respect-reconnect-false-and-clamp-retry
May 11, 2026
Merged

fix: respect reconnect=false and clamp server-supplied retry: 0#135
kinyoklion merged 1 commit into
mainfrom
rlamb/sdk-2347/respect-reconnect-false-and-clamp-retry

Conversation

@kinyoklion
Copy link
Copy Markdown
Member

@kinyoklion kinyoklion commented May 8, 2026

Summary

Stacked on top of #134. Addresses three pre-existing reconnect-control gaps surfaced during the multi-agent review of that PR.

  1. retry: 0 collapses backoff to zero. A server emitting retry: 0 set BackoffRetry::base_delay to Duration::ZERO, after which every reconnect path (client.rs:474, 525, 547, 612 and the new parse-error path) computed next_delay() == 0 and reconnected immediately — a tight loop. Clamps change_base_delay to a 1 ms floor.

  2. EOF arm ignored reconnect_opts.reconnect. The body-exhausted branch unconditionally scheduled WaitingToReconnect even when reconnect was disabled. Now honors the flag and transitions to StreamClosed, matching every other error path.

  3. Parse-error arm with reconnect=false left the parser poisoned. No state transition happened, so the next poll drained the broken body to EOF — where (2) above papered over the bug. Now transitions to StreamClosed so the documented "do not use the stream after error" contract holds.

Context

Stacked PR — base is the rl/sdk-2345/parser-error-reconnect-state branch from #134, not main. Will retarget once #134 lands.

Tracked in SDK-2347. Predecessor: SDK-2345 / #134.

Surfaced from the multi-agent review of #134 (findings 1, 2, and the corresponding suggested follow-ups 4 and 5). All three were pre-existing — #134 only made the parse-error path more visible.

Test plan

  • test_change_base_delay_clamps_to_minimum (retry.rs) — pins the 1 ms floor against change_base_delay(Duration::ZERO) and a sub-floor value.
  • parser_error_closes_stream_when_reconnect_disabled (client.rs) — asserts the stream emits one InvalidLine then None when reconnect is off.
  • eof_closes_stream_when_reconnect_disabled (client.rs) — asserts the stream emits the event, Eof, then None when reconnect is off.
  • Existing parser_error_schedules_reconnect_immediately from fix: schedule reconnect after parse error during streaming #134 still passes.
  • cargo test — 63 lib tests + 1 doc test pass.
  • cargo fmt --check clean.

Note

Medium Risk
Changes the SSE streaming state machine and retry backoff behavior, which can alter reconnect/termination semantics for consumers; scope is contained and covered by new tests.

Overview
Fixes reconnect disablement edge cases in the SSE client. When ReconnectOptions.reconnect is false, parse errors and clean EOF now transition the stream to StreamClosed so the next poll returns None instead of scheduling a reconnect or continuing with a poisoned parser.

Hardens retry backoff against server-supplied retry: 0. BackoffRetry::change_base_delay now clamps the base delay to a 1ms minimum to avoid zero-delay reconnect loops, with a new unit test and additional stream-level tests covering the new close-on-error/EOF behavior.

Reviewed by Cursor Bugbot for commit d33131a. Bugbot is set up for automated code reviews on this repo. Configure here.

/// Floor applied to a server-supplied SSE `retry:` value. A server that
/// sends `retry: 0` would otherwise collapse the backoff to zero and
/// reconnect would become a tight loop.
const MINIMUM_BASE_DELAY: Duration = Duration::from_millis(1);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want this larger.

Base automatically changed from rl/sdk-2345/parser-error-reconnect-state to main May 11, 2026 15:31
Three pre-existing reconnect-control gaps surfaced during the multi-
agent review of #134 (SDK-2347):

1. `BackoffRetry::change_base_delay` accepted any duration, including
   `Duration::ZERO`. A server emitting `retry: 0` collapsed the
   backoff to zero across every reconnect path, producing a tight
   reconnect loop. Clamp the input to a 1 ms floor.

2. The EOF arm of `ReconnectingRequest::poll_next` unconditionally
   scheduled a reconnect, even when `reconnect_opts.reconnect` was
   false. Honor the flag and transition to `StreamClosed` when
   reconnect is disabled, matching every other error path.

3. The parse-error arm only transitioned state when reconnect was
   enabled. With reconnect disabled, the parser stayed poisoned and
   the next poll drained to EOF, where (1) above papered over the
   bug. Transition to `StreamClosed` so the documented "do not use
   the stream after error" contract holds.

Tests:
- `test_change_base_delay_clamps_to_minimum` pins the retry floor.
- `parser_error_closes_stream_when_reconnect_disabled` asserts the
  stream returns `None` after a parse error when reconnect is off.
- `eof_closes_stream_when_reconnect_disabled` asserts the stream
  returns `None` after end-of-body when reconnect is off.
@kinyoklion kinyoklion force-pushed the rlamb/sdk-2347/respect-reconnect-false-and-clamp-retry branch from b2e0fb0 to d33131a Compare May 11, 2026 15:34
@kinyoklion kinyoklion marked this pull request as ready for review May 11, 2026 15:39
@kinyoklion kinyoklion requested a review from a team as a code owner May 11, 2026 15:39
@kinyoklion kinyoklion merged commit 80c123a into main May 11, 2026
8 checks passed
@kinyoklion kinyoklion deleted the rlamb/sdk-2347/respect-reconnect-false-and-clamp-retry branch May 11, 2026 15:39
kinyoklion pushed a commit that referenced this pull request May 11, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.17.4](0.17.3...0.17.4)
(2026-05-11)


### Bug Fixes

* respect reconnect=false and clamp server-supplied retry: 0
([#135](#135))
([80c123a](80c123a))
* schedule reconnect after parse error during streaming
([#134](#134))
([2ac2998](2ac2998))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: this PR only updates release metadata (version bump and
changelog) and does not modify runtime code paths.
> 
> **Overview**
> Bumps `eventsource-client` from `0.17.3` to `0.17.4` in
`.release-please-manifest.json` and `eventsource-client/Cargo.toml`.
> 
> Updates `eventsource-client/CHANGELOG.md` with the `0.17.4` release
notes (reconnect/retry behavior fixes and reconnect scheduling after
parse errors).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
cab73e7. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants