Skip to content

Conversation

@Ilevk
Copy link

@Ilevk Ilevk commented Jan 16, 2026

Description

Problem

  • x402HTTPAdapter doesn’t retry correctly when calling multiple different paid endpoints sequentially in the same session.
Bug Pattern:
session.get("/weather")         # 200 ✅
session.get("/premium/content") # 402 ❌ (no retry)

Root Cause

  • The adapter used an instance variable self._is_retry to track retry state. This flag persisted across requests, causing alternating requests to skip payment handling.

Solution

  • Replace instance-level _is_retry variable with per-request header X-x402-Payment-Retry
  • Use request.copy() to avoid mutating the original request
  • Return retry_response directly instead of copying attributes

This aligns with the patterns used in other language implementations (Go, httpx).

Flow Diagram

Before (Buggy)

sequenceDiagram
    participant Client
    participant Adapter
    participant Server

    Note over Adapter: _is_retry = False

    rect rgb(200, 255, 200)
        Note over Client,Server: Request 1 (Success)
        Client->>Adapter: send(request1)
        Adapter->>Server: HTTP GET
        Server-->>Adapter: 402 Payment Required
        Note over Adapter: _is_retry = True
        Adapter->>Server: HTTP GET + Payment
        Server-->>Adapter: 200 OK
        Adapter-->>Client: 200 OK
        Note over Adapter: _is_retry = True ⚠️ (not reset!)
    end

    rect rgb(255, 200, 200)
        Note over Client,Server: Request 2 (FAIL!)
        Client->>Adapter: send(request2)
        Note over Adapter: if _is_retry: return early
        Adapter->>Server: HTTP GET (no payment!)
        Server-->>Adapter: 402 Payment Required
        Adapter-->>Client: 402 ❌
        Note over Adapter: _is_retry = False
    end

    rect rgb(200, 255, 200)
        Note over Client,Server: Request 3 (Success)
        Client->>Adapter: send(request3)
        Note over Adapter: _is_retry = False
        Adapter->>Server: HTTP GET
        Server-->>Adapter: 402 Payment Required
        Note over Adapter: _is_retry = True
        Adapter->>Server: HTTP GET + Payment
        Server-->>Adapter: 200 OK
        Adapter-->>Client: 200 OK
    end
Loading

After (Fixed)

sequenceDiagram
    participant Client
    participant Adapter
    participant Server

    rect rgb(200, 255, 200)
        Note over Client,Server: Request 1
        Client->>Adapter: send(request1)
        Adapter->>Server: HTTP GET
        Server-->>Adapter: 402 Payment Required
        Note over Adapter: retry_request = request1.copy()
        Note over Adapter: retry_request.headers[RETRY_HEADER] = "1"
        Adapter->>Server: HTTP GET + Payment + RETRY_HEADER
        Server-->>Adapter: 200 OK
        Adapter-->>Client: 200 OK
    end

    rect rgb(200, 255, 200)
        Note over Client,Server: Request 2 (per-request state)
        Client->>Adapter: send(request2)
        Note over Adapter: Check request2.headers[RETRY_HEADER]
        Note over Adapter: No header → fresh request
        Adapter->>Server: HTTP GET
        Server-->>Adapter: 402 Payment Required
        Note over Adapter: retry_request = request2.copy()
        Note over Adapter: retry_request.headers[RETRY_HEADER] = "1"
        Adapter->>Server: HTTP GET + Payment + RETRY_HEADER
        Server-->>Adapter: 200 OK
        Adapter-->>Client: 200 OK ✅
    end

    rect rgb(200, 255, 200)
        Note over Client,Server: Request 3
        Client->>Adapter: send(request3)
        Adapter->>Server: HTTP GET
        Server-->>Adapter: 402 Payment Required
        Note over Adapter: retry_request = request3.copy()
        Adapter->>Server: HTTP GET + Payment + RETRY_HEADER
        Server-->>Adapter: 200 OK
        Adapter-->>Client: 200 OK ✅
    end
Loading

Tests

  • Added comprehensive unit tests in x402/tests/unit/http/clients/test_requests.py:
cd python/x402 && uv run pytest tests/unit/http/clients/test_requests.py -v

Test Cases:
- TestConsecutivePayments - Core bug fix verification
  - test_should_handle_all_consecutive_402_requests - 3 consecutive payments succeed
  - test_should_set_retry_header_on_retry_request - Retry header properly set
  - test_should_not_modify_original_request - Original request immutable
  - test_should_handle_mixed_200_and_402_requests - Free/paid endpoint mix
- TestBasicFunctionality - Non-402 responses, retry header prevention
- TestErrorHandling - PaymentError propagation
- TestFactoryFunctions - Factory function verification

Results:
15 passed in 0.09s

Regression Test: Verified tests fail with buggy code and pass with fix.

Checklist

  • I have formatted and linted my code
  • All new and existing tests pass
  • My commits are signed (required for merge)

Ilevk added 2 commits January 16, 2026 23:19
- Replace instance-level _is_retry flag with per-request header
  to prevent state leakage between consecutive 402 requests.
@vercel
Copy link

vercel bot commented Jan 16, 2026

@Ilevk is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions bot added sdk Changes to core v2 packages python labels Jan 16, 2026
@Ilevk Ilevk marked this pull request as ready for review January 16, 2026 14:40
@Ilevk
Copy link
Author

Ilevk commented Jan 16, 2026

@phdargen Hi Duke, I've done this job. please take a look when you have time.

@Ilevk Ilevk changed the title Fix/python v2 x402 http adapter retry fix: python v2 x402 http adapter retry Jan 16, 2026
@Ilevk Ilevk changed the title fix: python v2 x402 http adapter retry fix: python v2 x402HTTPAdapter retry logic Jan 16, 2026
@Ilevk
Copy link
Author

Ilevk commented Jan 16, 2026

I really appreciate a quick review :)

@phdargen
Copy link
Contributor

Looks great, thanks a lot for the fix @Ilevk 🚀

@phdargen phdargen merged commit 2b9a0d9 into coinbase:feat/python-v2-sdk Jan 16, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python sdk Changes to core v2 packages

Development

Successfully merging this pull request may close these issues.

2 participants