Skip to content

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

Draft
kinyoklion wants to merge 1 commit intorl/sdk-2345/parser-error-reconnect-statefrom
rlamb/sdk-2347/respect-reconnect-false-and-clamp-retry
Draft

fix: respect reconnect=false and clamp server-supplied retry: 0#135
kinyoklion wants to merge 1 commit intorl/sdk-2345/parser-error-reconnect-statefrom
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.

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.
/// 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.

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.

1 participant