test: pin down core HTTP retry behaviour - #850
Open
kraenhansen wants to merge 1 commit into
Open
Conversation
Characterises the default retry policy in core/http_client.py after a user reported a regression between v2.58.0 and v2.59.0, claiming cancelled in-flight requests were being retried. Splits the report into its two separable questions: whether cancellation is retried (it is not) and how many times a retryable response is retried (zero before, two after). Co-Authored-By: Claude <noreply@anthropic.com>
kraenhansen
marked this pull request as ready for review
August 20, 2026 08:50
PR SummaryCursor Bugbot is generating a summary for commit 7937286. Configure here. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A user reported a regression between
v2.58.0andv2.59.0: they cancel an in-flight upload (async task cancellation → connection teardown), typically after the body is sent but before the response is read, and believed the SDK now treats that cancellation as a retryable error and re-sends it.This adds characterisation tests for the retry policy in
core/http_client.pyso the two separable questions in that report are pinned down independently.Cancellation is not retried
_should_retry()takes anhttpx.Responseand only inspectsresponse.status_code. There is notry/exceptanywhere around the HTTP call — the only exception handling in the generated raw clients isJSONDecodeError/ValidationErrorfor response parsing. So any exception escapes before a retry decision is reached.Covered: a faithful reproduction of the reported scenario (transport reads the full body, then hangs; the awaiting task is cancelled),
CancelledErrorraised inside the transport, andReadError/WriteError/RemoteProtocolError/ConnectError/ReadTimeout. All assert exactly one attempt.Default retry count changed 0 → 2
This is the real behaviour change, and it explains the duplicate uploads they saw. The pre-
v2.59.0code was effectively dead:Effective retries were
max(0, max_retries - 2), so even an explicitmax_retries=2did nothing. It is nowretries: int = 0,max_retriesdefaulting to2, andretries < max_retries. This arrived via the Fern generator bump4.42.0→4.64.1in #817, not a hand-written change.test_retry_resends_the_full_request_bodypins the cost that makes the default matter: a503re-sends the entire body three times._should_retrykeys purely off status code with no method awareness, so this applies to 10+ multipart POST upload endpoints. Raised with Fern separately, along with a request for a client-widemax_retries(it currently only exists onRequestOptions).No source changes — this only documents current behaviour, and deliberately asserts the
0 → 2default so a future generator bump that changes it again fails loudly.Test plan
pytest tests/test_retry_behavior.py— 27 passedv2.58.0http_client.py: the 14 cancellation / transport-error / non-retryable tests pass unchanged, the 13 retry-count tests fail. Confirms cancellation handling is identical across versions and only the retry count regressed.mypy tests/test_retry_behavior.py— clean🤖 Generated with Claude Code