Skip to content

fetchWithRetry can't retry requests with a body (reuses a consumed Request) #196

Description

@pouyashahrdami

Bug

fetchWithRetry sends the same Request instance on every attempt. The first attempt consumes the body stream, so any retry of a body-carrying request (POST/PUT/PATCH) fails with Body has already been used instead of retrying.

This affects every execute tool API call: GlobalOutbound.fetch() passes a Request object into fetchWithRetry (src/tools/execute.tssrc/utils/fetch-retry.ts), so retry on rate limits is effectively broken for the server's write operations.

Observed behavior

  • After a 429: the loop sleeps through every backoff delay, each retry throws internally, and the original 429 response is returned anyway — the retries can never succeed.
  • After a network error: the retry throws TypeError: Body has already been used, masking the real network error.

GET requests (no body) are unaffected, and the existing tests in tests/fetch-retry.test.ts only use string URLs, which is why this was never caught.

Repro

const request = new Request('https://api.cloudflare.com/client/v4/accounts/xxx/d1/database', {
  method: 'POST',
  body: '{"name":"my-db"}'
})
// First attempt gets a 429 → every subsequent attempt fails with
// "Body has already been used" instead of resending the body.
await fetchWithRetry(request)

Fix

Send a Request.clone() on every attempt that can still be retried, letting the final attempt consume the original. PR incoming.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions