Conversation
Transport#request handled exactly one 402 response: it created a credential, retried once, and returned that retry's response directly regardless of what it was. Canonical mppx (src/client/internal/Fetch.ts) loops up to maxPaymentRetries (default 3) successive actionable challenges, since a server can legitimately respond to a submitted credential with a *fresh* 402 -- a different challenge, not success or a final failure (e.g. the original challenge expired mid-flight, or the credential was rejected but the server is willing to reissue). Against such a server, the Ruby transport recovered from zero of those retries and returned the second 402 to the caller unpaid, even though canonical mppx would have paid successfully. Wraps the existing single-attempt body in a loop bounded by a new Transport::MAX_PAYMENT_RETRIES = 3 constant (matching mppx's default). Each iteration re-parses WWW-Authenticate challenges from the latest response, so a fresh challenge on retry N is picked up on iteration N. All event emissions (challenge.received, credential.created, payment.response, payment.failed) are preserved exactly as before, just re-emitted per iteration instead of once. Rebased onto main's custom-header support (challenge.credential_header, from a change landed after this branch was first opened) -- each iteration re-reads credential_header from its own iteration's challenge, so a header choice that changes between challenges is respected on every retry. Behavior for a single 402 (the common case) is unchanged. A server that never accepts still gets bounded by the retry limit rather than looping forever, and a non-actionable 402 (no matching method, unparseable/expired challenge) still breaks out of the loop and returns immediately as before. Adds two regression tests in test/mpp/test_client.rb: - test_retries_across_multiple_402_challenges: a server that rejects the first credential with a second, different challenge succeeds on the third request (3 total HTTP requests). - test_gives_up_after_max_payment_retries: a server that always returns 402 is called exactly 1+MAX_PAYMENT_RETRIES times, then the transport returns that last 402 rather than looping forever. I could not run the test suite in my sandbox -- no access to rubygems.org to install bundler/webmock/minitest -- but `ruby -c` confirms both files parse cleanly. Please run `bundle exec rake test` before merging. Fixes tempoxyz/mpp-tools#174
ygd58
force-pushed
the
fix/client-retry-multiple-402s
branch
from
September 9, 2026 08:08
cf8c3ba to
b9068ad
Compare
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.
What
Transport#requesthandled exactly one 402 response: it created a credential, retried once, and returned that retry's response directly regardless of what it was. Canonical mppx (src/client/internal/Fetch.ts) loops up tomaxPaymentRetries(default 3) successive actionable challenges, since a server can legitimately respond to a submitted credential with a fresh 402 — a different challenge, not success or a final failure (e.g. the original challenge expired mid-flight, or the credential was rejected but the server is willing to reissue). Against such a server, the Ruby transport recovered from zero of those retries and returned the second 402 to the caller unpaid, even though canonical mppx would have paid successfully.Flagged by the cross-SDK audit as AGR-2026-063. I already fixed the identical pattern in
tempoxyz/mpp-go(PR #118) — same root cause, same fix shape, different language.Fix
Wraps the existing single-attempt body in a loop bounded by a new
Transport::MAX_PAYMENT_RETRIES = 3constant (matching mppx's default). Each iteration re-parsesWWW-Authenticatechallenges from the latest response, so a fresh challenge on retry N is picked up on iteration N. All event emissions (challenge.received,credential.created,payment.response,payment.failed) are preserved exactly as before, just re-emitted per iteration instead of once.Behavior for a single 402 (the common case) is unchanged. A server that never accepts still gets bounded by the retry limit rather than looping forever, and a non-actionable 402 (no matching method, unparseable/expired challenge) still breaks out of the loop and returns immediately as before.
Testing
Added two regression tests in
test/mpp/test_client.rb:test_retries_across_multiple_402_challenges: a server that rejects the first credential with a second, different challenge succeeds on the third request (3 total HTTP requests, verified viaassert_requested).test_gives_up_after_max_payment_retries: a server that always returns 402 is called exactly1+MAX_PAYMENT_RETRIEStimes, then the transport returns that last 402 rather than looping forever.I could not run the test suite in my sandbox — no access to rubygems.org to install bundler/webmock/minitest — but
ruby -cconfirms both files parse cleanly, and I reviewed the diff carefully: outside the loop restructuring and apayment_response→responserename, the only semantic changes arereturn→breakat the three early-exit points. Please runbundle exec rake testbefore merging, happy to fix anything that doesn't match.Fixes tempoxyz/mpp-tools#174