Skip to content

fix(client): handle multipart and streaming bodies on paid 402 retry#166

Open
bennytimz wants to merge 1 commit into
tempoxyz:mainfrom
bennytimz:fix/client-retry-body-multipart
Open

fix(client): handle multipart and streaming bodies on paid 402 retry#166
bennytimz wants to merge 1 commit into
tempoxyz:mainfrom
bennytimz:fix/client-retry-body-multipart

Conversation

@bennytimz

@bennytimz bennytimz commented Jul 3, 2026

Copy link
Copy Markdown

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: PaymentTransport was reusing the already-consumed request.stream on 402 retry, so any POST/PUT/PATCH body was silently dropped. The fix is await request.aread() before dispatch and content=request.content (instead of stream=request.stream) on retry.

What this PR adds on top

1. Multipart / files= bodies (tested)

aread() works for MultipartStream because it implements both SyncByteStream and AsyncByteStream. 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_bodyfiles= upload, verifies body and filename are present in the retry
  • test_paid_retry_replays_put_body — PUT with a bytes body
  • test_paid_retry_replays_patch_body — PATCH with a bytes body

Also includes test_paid_retry_replays_request_body (same as #156's test) so this branch is self-contained.

2. Async generator bodies — explicit PaymentError

content=async_gen() in httpx produces an AsyncIteratorByteStream which is AsyncByteStream but not SyncByteStream. Unlike MultipartStream and ByteStream, an async generator:

  • may be infinite or tied to a one-shot I/O source
  • may already be partially consumed before the transport receives it
  • silently buffering it would break the streaming contract and could OOM

PaymentTransport.handle_async_request now detects this case before any I/O and raises PaymentError with a clear message directing callers to use a buffered body instead.

New test:

  • test_paid_retry_raises_for_streaming_body — verifies PaymentError is raised immediately with no requests sent

Detection logic

if isinstance(request.stream, httpx.AsyncByteStream) and not isinstance(
    request.stream, httpx.SyncByteStream
):
    raise PaymentError("Streaming request bodies (async generators) are not supported ...")

ByteStream and MultipartStream are both Sync+Async so they pass through. Only AsyncIteratorByteStream (async generators) is async-only and is caught.

Test plan

  • All 28 tests/test_client.py tests pass (confirmed locally)
  • No regressions in the rest of the test suite
  • Multipart body is identical byte-for-byte on the initial and retry requests
  • Async generator body raises PaymentError before any network I/O

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