fix(client): handle multipart and streaming bodies on paid 402 retry#166
Merged
brendanjryan merged 1 commit intoJul 10, 2026
Merged
Conversation
7 tasks
Collaborator
|
LGTM -- thanks for the fix |
Collaborator
|
@bennytimz - can you add a changelog? |
Complements PR tempoxyz#156 (bytes/content= body replay) with two additions: 1. Multipart / files= bodies: aread() is called before the first dispatch, buffering the MultipartStream into request._content. The paid retry uses content=request.content so the full multipart payload is replayed. Tests: test_paid_retry_replays_multipart_body, test_paid_retry_replays_put_body, test_paid_retry_replays_patch_body. 2. Async generator bodies: an AsyncByteStream that is not also a SyncByteStream (i.e. AsyncIteratorByteStream from content=async_gen()) cannot be safely buffered for replay — the generator may be infinite, already partially consumed, or tied to a one-shot I/O source. PaymentTransport now raises PaymentError immediately with a clear message directing callers to use a buffered body instead. Test: test_paid_retry_raises_for_streaming_body. Also includes the core fix from tempoxyz#156: await request.aread() before dispatch and content=request.content (not stream=request.stream) on retry, so that all finite body types are correctly replayed.
d207c01 to
654d076
Compare
Contributor
Author
|
Added, thanks for the review @brendanjryan |
bennytimz
added a commit
to bennytimz/pympp
that referenced
this pull request
Jul 7, 2026
Adds RetryPolicy and wires it into PaymentTransport and Client.
RetryPolicy (src/mpp/client/retry.py):
- max_attempts=3 with backoff_delays=[0.5, 1.0] seconds by default
- Retries on status codes in retryable_status_codes (500/502/503/504)
- Optionally retries on httpx.ConnectError / httpx.RemoteProtocolError
- Exported from mpp and mpp.client for easy import
PaymentTransport:
- New retry_policy: RetryPolicy | None = None parameter
- _dispatch() helper runs the retry loop before 402 handling so that
transient 5xx errors are resolved before the payment flow begins
- asyncio.sleep provides the configurable backoff between attempts
- Each retry attempt logs a warning with attempt number and URL
- Async-generator bodies (guarded by PR tempoxyz#166) propagate before the
retry loop, so no unsafe body replay can occur
Client:
- retry_policy defaults to RetryPolicy() (retry on by default)
- Pass retry_policy=None to get raw no-retry behaviour
Tests (tests/test_client.py):
- test_5xx_retried_with_backoff: 503x2 then 200; verifies sleep(0.5) and sleep(1.0)
- test_5xx_exhausted_returns_last_response: max_attempts=2, always 503
- test_connect_error_retried: ConnectError then 200
- test_retry_disabled_when_policy_is_none: PaymentTransport with retry_policy=None
- test_non_retryable_5xx_not_retried: 501 passes through in one attempt
- test_client_retry_disabled_when_policy_is_none: Client(retry_policy=None) via HTTPXMock
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.
Summary
This PR complements #156 (bytes/
content=body replay) and addresses the two body-type gaps that #156 does not cover.What #156 fixed
PR #156 by @Tleaoo fixes the core bug:
PaymentTransportwas reusing the already-consumedrequest.streamon 402 retry, so any POST/PUT/PATCH body was silently dropped. The fix isawait request.aread()before dispatch andcontent=request.content(instead ofstream=request.stream) on retry.What this PR adds on top
1. Multipart /
files=bodies (tested)aread()works forMultipartStreambecause it implements bothSyncByteStreamandAsyncByteStream. The full multipart-encoded bytes are buffered before the first dispatch, and the paid retry sends the identical payload.New tests:
test_paid_retry_replays_multipart_body—files=upload, verifies body and filename are present in the retrytest_paid_retry_replays_put_body— PUT with a bytes bodytest_paid_retry_replays_patch_body— PATCH with a bytes bodyAlso includes
test_paid_retry_replays_request_body(same as #156's test) so this branch is self-contained.2. Async generator bodies — explicit
PaymentErrorcontent=async_gen()in httpx produces anAsyncIteratorByteStreamwhich isAsyncByteStreambut notSyncByteStream. UnlikeMultipartStreamandByteStream, an async generator:PaymentTransport.handle_async_requestnow detects this case before any I/O and raisesPaymentErrorwith a clear message directing callers to use a buffered body instead.New test:
test_paid_retry_raises_for_streaming_body— verifiesPaymentErroris raised immediately with no requests sentDetection logic
ByteStreamandMultipartStreamare both Sync+Async so they pass through. OnlyAsyncIteratorByteStream(async generators) is async-only and is caught.Test plan
tests/test_client.pytests pass (confirmed locally)PaymentErrorbefore any network I/O