# Errors & remediation

Every error response has the same shape:

```json
{ "error": "human-readable message" }
```

Use the HTTP status code to branch; use the message for the user-facing diagnostic.

## 400 Bad Request

The request body is malformed or fails server-side validation.

| Common message                                            | Cause | Fix |
|-----------------------------------------------------------|-------|-----|
| `name e url sono obbligatori` (MCP) / `name is required` (keys) | Missing required field | Re-send with the missing field |
| `Invalid JSON body`                                       | Body is not valid JSON or `Content-Type: application/json` is missing | Add the header, validate the JSON |
| `auth.jwksJson: <reason>`                                 | JWKS document fails structural validation | Re-fetch from the IdP; run `POST /api/mcp/jwks/test` first |
| `<rule field>: <reason>`                                  | Rule schema rejected the rule | The message names the offending field; consult [rules-and-dlp.md](rules-and-dlp.md) |
| `Invalid name — use letters, numbers, - and _ only`       | Key name doesn't match `[a-zA-Z0-9_-]+` | Sanitise the name |
| `URI already in use on this MCP server`                   | Resource URI collision | Pick a different URI or PATCH the existing one |
| `before must be a YYYYMMDD integer`                       | Bad query param on usage/log delete | Pass `20260115`, not `2026-01-15` |
| `tools[N] not found on this MCP server`                   | Granting a permission tag for a tool that wasn't discovered | Run `POST /api/mcp/:id/tools` first to refresh the catalog |

## 401 Unauthorized

You are not authenticated. Two flavours:

| Message | Meaning |
|---------|---------|
| `Not authenticated` | No PAT (or session cookie) on the request |
| `Invalid personal access token` | A Bearer header was sent but the token is unknown or has been rotated |
| `MFA required` | Session-only flow with MFA pending — only relevant when called from the dashboard, not from a PAT |

Fix: re-export `AIRONCLAW_TOKEN`. If the user rotated, they need to copy the new plaintext from the dashboard. PATs are single-instance per user — issuing a new one invalidates the previous immediately.

## 403 Forbidden

You are authenticated but not allowed.

| Message | Cause | Fix |
|---------|-------|-----|
| `This endpoint does not accept personal access tokens. Use the dashboard session.` | You hit a session-only endpoint with a PAT (`/api/2fa/*`, `/api/profile/token`) | Direct the user to the dashboard for that action |
| `MCP limit reached (N/M). ...` | User-quota exceeded | Ask the user to delete an unused MCP or contact admin |
| `API key limit reached (N/M). ...` | Same, for keys | Same |
| `Forbidden` (on a key PATCH/DELETE) | The key id belongs to a different consumer | Re-list keys, you got the wrong id |
| `Cannot modify personal token` | Trying to PATCH/DELETE the PAT key-auth credential | Use `POST /api/profile/token` from the dashboard to rotate |

## 404 Not Found

| Message | Cause |
|---------|-------|
| `MCP server not found` / `LLM proxy not found` | The id is not in the caller's owned set (or has been deleted) |
| `Key not found` | Same, for client keys |
| `Resource not found` | The MCP exists but the resource id doesn't |
| `Endpoint not found` | The path is wrong; recheck against [api-reference.md](api-reference.md) |

If the user just created the resource and you still get 404, give Redis a moment and re-list — the dashboard caches GETs but the API is no-store.

## 409 Conflict

| Message | Cause |
|---------|-------|
| `2FA is already enabled` / `2FA is not enabled` | Wrong order of TOTP enrolment / disable steps |
| `Key is missing a name tag — recreate it to change permissions` | Legacy key without the `name:` tag — delete and recreate |

## 429 Too Many Requests

Currently only emitted by `/api/2fa/challenge` (10 attempts / 5 min per user). The other endpoints are not rate-limited at the management API level.

If you're hitting 429 on **proxy** traffic (not the management API), that's a `rate_limit` rule firing on the MCP/LLM — check `GET /api/mcp/:id/rules` (or `/api/llm/:id/rules`) to see which rule, and the `Retry-After` response header (or `ban_timespan` if a ban kicked in).

## 500 / 502 / 503

| Status | Common cause | What to do |
|--------|--------------|-----------|
| `500` `Failed to create the key` / `Failed to update permissions` | the gateway Admin returned non-2xx | Check `/api/health` for gateway status; retry once |
| `502` (from `POST /api/mcp/:id/tools`) | Upstream MCP unreachable or returned non-JSON | Verify the MCP `url` resolves and accepts the upstream bearer; the message body relays the upstream error |
| `503` `Redis unavailable` | Redis is down | Check `/api/health`; this is an infra outage, not a client problem |

For all 5xx, do **not** retry rapidly. One retry after a few seconds is fine; if it persists, surface to the user as an infra issue.

## Rate-limit and ban events

If a request is blocked by an `ip_acl` rule with `action: deny`, you get a 403 from the gateway itself (not from the management API). The audit log (`GET /api/logs?event_type=ip_blocked` etc.) is the source of truth for what happened.

If a `rate_limit` rule with `ban_after_n_exceeded` fired, the IP is banned for `ban_timespan` seconds. The first request that finds the ban gets a 403 with body `IP address temporarily banned`. The ban is keyed on the `match_key` value (e.g. specific IP).

To inspect bans, query the audit log with `event_type=ip_banned`.

## When the response is empty or hangs

The management API does not stream — every endpoint returns a single JSON document. If a request hangs > 5s without a response, it is almost certainly a network problem between you and `${AIRONCLAW_BASE_URL}`, not the API. Verify reachability with `curl -fsS -o /dev/null -w '%{http_code}\n' ${AIRONCLAW_BASE_URL}/api/health -H "Authorization: Bearer ${AIRONCLAW_TOKEN}"`.
