Skip to content

Commit d384f96

Browse files
committed
fix: reset request body before x402 retry to prevent exhausted reader
X402Transport.RoundTrip clones the request twice (first attempt + retry) but Clone() shares the body reader without calling GetBody(). After the first attempt consumes the body, the retry gets an exhausted reader, causing 'ReverseProxy does an invalid Read on closed Body'. Add bodyResetTransport that calls GetBody() before each RoundTrip, ensuring each attempt gets a fresh reader. Pairs with the existing bodyBufferMiddleware which guarantees GetBody is always set.
1 parent 4787287 commit d384f96

1 file changed

Lines changed: 20 additions & 1 deletion

File tree

internal/x402/buyer/proxy.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ func (p *Proxy) buildUpstreamHandler(name, remoteModel string, cfg UpstreamConfi
217217
return nil
218218
},
219219
Transport: &x402http.X402Transport{
220-
Base: http.DefaultTransport,
220+
Base: &bodyResetTransport{base: http.DefaultTransport},
221221
Signers: []x402.Signer{signer},
222222
Selector: x402.NewDefaultPaymentSelector(),
223223
OnPaymentAttempt: func(event x402.PaymentEvent) {
@@ -336,6 +336,25 @@ func bodyBufferMiddleware(next http.Handler) http.Handler {
336336
})
337337
}
338338

339+
// bodyResetTransport resets the request body from GetBody before each RoundTrip.
340+
// This works around a bug in x402-go's X402Transport where req.Clone() shares
341+
// the body reader — after the first attempt consumes it, the retry gets an
342+
// exhausted reader. By resetting from GetBody (set by bodyBufferMiddleware),
343+
// each attempt gets a fresh reader.
344+
type bodyResetTransport struct {
345+
base http.RoundTripper
346+
}
347+
348+
func (t *bodyResetTransport) RoundTrip(req *http.Request) (*http.Response, error) {
349+
if req.GetBody != nil {
350+
body, err := req.GetBody()
351+
if err == nil {
352+
req.Body = body
353+
}
354+
}
355+
return t.base.RoundTrip(req)
356+
}
357+
339358
// handleStatus returns JSON with remaining auths and spend per upstream.
340359
func (p *Proxy) handleStatus(w http.ResponseWriter, r *http.Request) {
341360
p.mu.RLock()

0 commit comments

Comments
 (0)