Skip to content

fix(fetch): retry transient errors with exponential backoff - #4637

Open
magai2002 wants to merge 1 commit into
modelcontextprotocol:mainfrom
magai2002:fix-4449-fetch-retry-backoff
Open

fix(fetch): retry transient errors with exponential backoff#4637
magai2002 wants to merge 1 commit into
modelcontextprotocol:mainfrom
magai2002:fix-4449-fetch-retry-backoff

Conversation

@magai2002

Copy link
Copy Markdown

Description

The fetch MCP server returns transient HTTP errors (429, 5xx) and network errors straight to the calling agent, with no retry. A short backoff would clear most of these, but every caller has to reimplement that logic itself.

Server Details

  • Server: fetch
  • Changes to: internal retry/backoff behavior of the fetch tool and prompt, environment variables

Motivation and Context

Closes #4449. Transient errors are the norm for public APIs and documentation sites under load. Handling retries in the server keeps agent prompts clean and avoids wasted LLM turns handling errors a retry would have resolved.

  • Retryable: 429, 500, 502, 503, 504, and network-level errors (connection reset, timeout, DNS failure, etc. — anything httpx raises as HTTPError)
  • Non-retryable: everything else (400, 401, 403, 404, 405, 409, 410, 422, ...) — fails on the first attempt, no wasted delay
  • Default policy: 3 attempts total, 1s initial delay, 2x backoff factor, capped at 10s
  • A numeric Retry-After header on a 429 response overrides the backoff delay for that wait; a non-numeric value (e.g. an HTTP-date) falls back to the computed backoff
  • Configurable via FETCH_MAX_RETRIES and FETCH_RETRY_DELAY_MS environment variables

Scope note: the fetch server only ever issues GET requests, so the "don't retry non-idempotent methods" concern from the issue doesn't apply here — there's nothing else to opt out of.

How Has This Been Tested?

  • 9 new tests: retry-then-succeed on a 503, non-retryable 404 fails fast with no sleep, Retry-After override, network-error (HTTPError) retry, and direct unit tests of the backoff-delay helper (exponential growth, max-delay cap, numeric vs. non-numeric Retry-After). uv run pytest: 29 passed, all fast (asyncio.sleep is mocked so no test actually waits).
  • uv run ruff check .: clean. uv run pyright: 0 errors.
  • Manually smoke-tested that FETCH_MAX_RETRIES / FETCH_RETRY_DELAY_MS reach serve() correctly, and that defaults (3 / 1000ms) apply when unset.
  • Have not tested this against a live LLM client — happy to if wanted before merge.

Breaking Changes

Behavioral, not interface-breaking: a request that previously failed immediately on a transient error will now retry up to 3 times before failing, adding up to ~3s of latency in the worst case. No config changes required for existing users; opt out isn't currently exposed (FETCH_MAX_RETRIES=1 achieves the same effect if needed).

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly
  • I have tested this with an LLM client
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options

Additional context

This PR is independent of #4636 (the --timeout PR, also against fetch) — built on a separate branch off current main, no shared commits. They touch adjacent but non-overlapping code in fetch_url/serve, so either can merge first without blocking the other; happy to rebase if needed.

The fetch server returned 429/5xx and network errors straight to the
agent with no retry, forcing every caller to reimplement its own retry
logic for errors that a short backoff would usually clear.

Retry 429/500/502/503/504 and network errors with exponential backoff:
3 attempts by default, 1s initial delay, doubling each retry, capped at
10s. A numeric Retry-After header on a 429 is honored over the backoff
delay. Non-retryable statuses (404, 401, etc.) still fail immediately.
Configurable via FETCH_MAX_RETRIES and FETCH_RETRY_DELAY_MS env vars.

Closes modelcontextprotocol#4449
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.

fetch: no retry/backoff on transient 429/5xx or network errors

1 participant