# Errors

The error body of the Filudi API, its nine codes and the status of each, and the two routes that answer another shape.

Every error is JSON with two fields and sometimes a third:

```json
{
  "error": "invalid",
  "message": "nickname: 3 to 20 characters.",
  "details": { "field": "nickname" }
}
```

`error` is one of nine codes. `message` is written for the developer who reads the response, in English, and
says what failed. `details` is present when the API has something structured to add, such as the field at
fault.

<CodePanel title="Read an error">
  <Fragment slot="curl">

    ```sh
    curl -s -o body.json -w '%{http_code}\n' \
      -X PATCH 'https://api.filudi.com/v1/sdk/players/me' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \
      -H 'content-type: application/json' \
      -d '{ "nickname": "a" }'
    cat body.json
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    const response = await fetch('https://api.filudi.com/v1/sdk/players/me', {
      method: 'PATCH',
      headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' },
      body: JSON.stringify({ nickname: 'a' }),
    });
    if (!response.ok) {
      const { error, message, details } = await response.json();
      if (error === 'unauthorized') forgetToken();
      if (error === 'rate_limited') waitSeconds(Number(response.headers.get('retry-after')));
      console.warn(error, message, details);
    }
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    response = requests.patch(
        'https://api.filudi.com/v1/sdk/players/me',
        headers={'authorization': f'Bearer {player_token}'},
        json={'nickname': 'a'},
    )
    if not response.ok:
        body = response.json()
        print(response.status_code, body['error'], body['message'])
        if body['error'] == 'rate_limited':
            retry_after = int(response.headers['retry-after'])
    ```

  </Fragment>
</CodePanel>

## Codes

| Code           | Status | When                                                                                                                           |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `invalid`      | 400    | The body or a parameter breaks the schema. `message` names the field                                                           |
| `unauthorized` | 401    | No credential, or one the API refuses. For a player token: forget it and create a new guest                                    |
| `forbidden`    | 403    | The credential is valid and does not open this action: another game's token, a role, a scope, an origin                        |
| `not_found`    | 404    | Unknown resource, or one your credential may not see. An unknown site slug and a game outside a site's catalog answer this too |
| `conflict`     | 409    | The state refuses the change: a slug taken, a version already live, a quota reached                                            |
| `too_large`    | 413    | The body is over the route's limit (64 kB for a save or an event batch)                                                        |
| `rate_limited` | 429    | Too many requests. `Retry-After` says how many seconds to wait; see [Rate limits](/docs/api/rate-limits/)                      |
| `unavailable`  | 503    | A dependency is down, usually Redis. Retry in a few seconds; `Retry-After` is set when the API knows                           |
| `internal`     | 500    | An unexpected failure. The API logged it                                                                                       |

The write routes of the admin API answer 400 `invalid`, 403 `forbidden`, 404 `not_found` and 409 `conflict`
with this shape even where the reference lists only the success code.

## Two routes with another body

`PUT /v1/sdk/saves/{gameId}` answers 409 with the server's copy of the save, not an error: `{ gameId, version,
etag, bytes, updatedAt, data }`, with `version` 0 and `data` null when nothing is stored yet. Take it, merge if
the game can, and write again with `If-Match` set to that version.

`POST /v1/sdk/leaderboards/{gameId}/{board}/scores` answers 400 with `{ error: "invalid", reason, message }`.
`reason` is one of `bounds`, `signature`, `session`, `board`, `nonce`, `clock`, `meta`, so a game logs why a
score was refused.

## Details worth reading

- `details.field: "current"` on a 401 of `PUT /v1/account/password` and `PUT /v1/console/password`: the
  session or token is fine, the current password is wrong. Keep the token.
- The issue code `game_host` on a 400 of `POST /v1/admin/sites` and its PATCH: a domain is on or under the
  play domain, where no site may run.
- A 403 of `PUT /v1/admin/sites/{siteId}/catalog/{gameId}` carries in `message` why the site may not list the
  game: pending, no live version, or `first_party` on a site that is not first-party.
