# Designing a resilient client

Only two failures here are worth retrying: a 429 and a genuine transport fault. Everything else — 400, 403, 404, 405, 413, 422 and every JSON-RPC error code — is an answer, and repeating the request repeats the answer. A tool that fails is different again: it returns 200 with `isError` set, not an HTTP error.

- **Status:** Available
- **Audience:** developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/developers/errors-and-retries/

## The failures, sorted

| Response | Where | Meaning | Retry? |
|---|---|---|---|
| `429` + `{"error":{"code":"rate_limited"}}` | `/api/public/*` | More than 240 requests in the last minute from your address | Yes, after waiting |
| `429`, JSON-RPC `-32603` | `/mcp` | More than 120 calls in the last minute | Yes, after waiting |
| `404` + `{"error":{"code":"not_found"}}` | `/api/public/*` | No page or manifest by that reference | No — fix the reference |
| `422` | `/api/public/*` | A parameter outside its documented range; `q` under two characters, `limit` above 25 | No |
| `403`, JSON-RPC `-32600` | `/mcp` | An `Origin` header was present and is not allowed | No |
| `405` with `Allow: POST` | `/mcp` | A GET or DELETE. This revision has no GET stream and no sessions | No |
| `413` | `/mcp` | Body over 256 KiB | No |
| `400`, JSON-RPC `-32700` | `/mcp` | The body is not valid JSON | No |
| `400`, JSON-RPC `-32020` | `/mcp` | A header disagrees with the body — version, method or name | No |
| `404`, JSON-RPC `-32601` | `/mcp` | Unknown method | No |
| `200` with `isError: true` | `/mcp` tool results | The tool ran and could not answer — an empty query, an unknown page | No |
| Connection reset, timeout, `5xx` | Anywhere | Transport or server fault | Yes |

Sorting failures this way is the whole of resilience. A client that retries a 404 turns one wrong reference into a hundred, and a client that does not retry a reset turns a two-second network hiccup into a user-visible error.

## Backing off without a Retry-After

No `Retry-After` header is documented on either surface, so the schedule is yours to choose. Two facts constrain it usefully: the limiter counts requests in a rolling sixty-second window, and it is measured on a monotonic clock, so the window is honest even across a clock adjustment.

1. On a 429, wait before the next attempt. A rolling window drains as your oldest requests age out, so a short wait may be enough — but retrying immediately guarantees another 429 and adds to the count you are trying to get under.
2. Use exponential backoff with full jitter: `sleep = random(0, min(cap, base × 2^attempt))`. Jitter matters more than the base here, because clients that back off in lockstep re-collide at every step.
3. Cap total attempts, not just the delay. Three or four is right for a read that a person is waiting on.
4. Treat a repeated 429 after several attempts as a signal to slow the whole client down, not to retry harder — the limit exists to stop a loop.

Every public route is a GET and every public MCP tool is annotated `idempotentHint: true`, so a retry can never duplicate anything. That is unusually comfortable, and it is a property of a read-only surface rather than a promise that would extend to a write.

## Two errors that look like faults and are not

**`-32020`, a header mismatch** — The `MCP-Protocol-Version`, `Mcp-Method` or `Mcp-Name` header did not match the request body. It exists so that a load balancer routing on a header and a server executing on the body cannot see two different requests. Fix the client — usually a stale header on a reused connection object — rather than retrying.
**`405` on a GET to `/mcp`** — Not a misconfiguration. Protocol revision 2026-07-28 removed the standalone GET stream and protocol-level sessions, so a client that opens one is told plainly instead of waiting on a stream that will never carry anything. The response names the revision and sets `Allow: POST`.

A third case is worth naming because it is silent: sending `Mcp-Session-Id` or `Last-Event-ID`. Both are ignored, not rejected. If your client depends on a session it thinks it has, nothing will tell you — the requests simply behave as independent calls, which they are.

## What to log when something fails

Log the route or JSON-RPC method, the numeric code, `error.code` when there is one, and the request id you sent. That set is enough for anyone to reconstruct the call. Do not log the full response body of a documentation fetch — it can be sixty thousand characters of Markdown, and none of it helps.

Never log a credential, and note the trap: an integration key does not belong on these routes at all. A key sent to a documentation endpoint authenticates nothing and travels into every log and proxy along the way.

If the failure survives a careful retry, [getting help as a developer](/developers/developer-support/) lists what a report needs to be actionable on the first reply.

## Questions

### Is a JSON-RPC error also an HTTP error?

Sometimes, and the pairing is deliberate. Transport-level problems carry a matching HTTP status — 400, 403, 405, 413, 429. An unknown tool, resource or prompt answers 200 with a JSON-RPC error, because the transport worked and the request was understood.

### How do I tell a tool failure from a transport failure?

A tool failure is a successful response whose `result` carries `isError: true` and a text explanation. A transport failure has no `result` at all. Check for `result` first, then for `isError` inside it.

### Should I retry a 403 from the MCP endpoint after removing my Origin header?

That is a fix rather than a retry, and yes, it works: a request with no `Origin` is accepted, because a non-browser client sends none. Only a present, invalid one is refused.

## Related

- [API error shapes](https://connectbyjbrh.com/developers/api-errors/)
- [MCP error shapes](https://connectbyjbrh.com/developers/mcp-errors/)
- [API rate limits](https://connectbyjbrh.com/developers/api-rate-limits/)
- [Outbound webhooks](https://connectbyjbrh.com/developers/webhooks-outbound/)
- [Getting help as a developer](https://connectbyjbrh.com/developers/developer-support/)

## What this page is based on

- `backend/app/mcp_server.py` — error codes, statuses, header checks, limits
- `backend/app/public_developer_api.py` — `_error`, `_guard`, RATE_MAX
- `docs-source/facts.py` — PROTOCOLS['mcp'], PROTOCOLS['jsonrpc']
