# Errors and limits

Every failure is an [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) `application/problem+json` response. Images and errors never mix: if you asked for a PNG and the status is not 200 (or 304), the body is JSON.

## The shape

```text
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://wherebox.dev/docs/errors#invalid-spec",
  "title": "Invalid map spec",
  "status": 400,
  "detail": "The request does not match the map spec",
  "issues": [{ "path": "fade", "message": "…" }]
}
```

`type` is a stable code, `title` a short human label, `status` repeats the HTTP status, and `detail` says what happened. Some errors add fields: `issues` on 400, `suggestions` on 404, `candidates` on 409. The trailing part of `type` (after `#`) is the code, and every code has an entry in the [error reference](/docs/errors.md), which is generated from the same table the server uses. The part before `#` is the address of that entry in the published docs; a self-hosted server can point it at its own copy with `WHEREBOX_DOCS_URL`, so match on the code, not on the whole URL.

## Branch on `type`, not on text

`title` and `detail` are for people and may be reworded. `type` and `status` are the contract. A client usually needs three branches:

```js
const res = await fetch(url);
if (res.ok) return res;                       // 200 image, or 304 with If-None-Match

const problem = await res.json();
const code = problem.type.split('#')[1];      // "ambiguous", "busy", "invalid-spec", …

switch (code) {
  case 'ambiguous':     // 409: let the user pick problem.candidates[i].id, retry with area=<id>
  case 'not-found':     // 404: offer problem.suggestions
  case 'invalid-spec':  // 400: a bug in your request; log problem.issues
  case 'busy':          // 503: retry, see below
  default:              // upstream-failed/-timeout (502/504): retry later or fall back
}
```

Which code means what, in one line each:

| Status | Code | What to do |
|---|---|---|
| 400 | [`invalid-spec`](/docs/errors.md#invalid-spec) | Fix the request. `issues[]` lists each bad parameter by path; unknown parameters come with a "did you mean" hint. Not retryable. |
| 400 | [`bad-request`](/docs/errors.md#bad-request) | Something else is unacceptable as sent: a rejected icon or font, an unsupported extension, routing not configured. Not retryable. |
| 404 | [`not-found`](/docs/errors.md#not-found) | No such area or place. Use `suggestions`, or look it up with `/v1/areas/search`. |
| 409 | [`ambiguous`](/docs/errors.md#ambiguous) | Several areas match; see [Locator maps](/docs/guides/locator-maps.md#ambiguous-names). |
| 422 | [`no-route`](/docs/errors.md#no-route) | The routing engine found no route for that profile. |
| 503 | [`busy`](/docs/errors.md#busy) | The render queue is full. Retry. |
| 502 | [`upstream-failed`](/docs/errors.md#upstream-failed) | A basemap, tile or routing server failed. Retry later. |
| 504 | [`upstream-timeout`](/docs/errors.md#upstream-timeout) | A routing or asset server was too slow. Retry later. |

### Ambiguous names in practice

A name that matches several areas is the one error you should expect in normal use. This request is parsed in the tests, but it is `409` when sent:

```http
GET /v1/map.png?area=Nová+Ves&w=400&h=300
```

The response carries a `candidates` array; each entry has an `id`, an `adminLevel` and a `context`. Show the contexts to a person, then repeat the request with `area=<id>`. Or narrow it up front with `level` and `country`.

### Invalid parameters

Unknown parameters are rejected rather than ignored, so a typo cannot silently produce a different map. `GET /v1/map.png?aera=Czechia` returns `400 invalid-spec` with an issue like `unknown parameter "aera" (did you mean "area"?)`. Out-of-range numbers and malformed colors are reported the same way, each with its `path`.

## Retries on 503

When more renders are requested than the server can run and queue, it answers `503` with `busy` and a `Retry-After` header (in seconds). Retry after that long, with a little jitter, and give up after a few attempts. Retry `502` and `504` the same way. Do not retry `400`, `404`, `409` or `422`: the answer will not change.

Identical concurrent requests share a single render, and a repeated request is served from the cache without taking a render slot, so retry storms on one URL are cheap.

```js
async function fetchMap(url, tries = 4) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url);
    if (res.status !== 503 && res.status !== 502 && res.status !== 504) return res;
    const wait = Number(res.headers.get('retry-after') ?? 2) * 1000 * (i + 1);
    await new Promise((r) => setTimeout(r, wait + Math.random() * 250));
  }
  throw new Error('map service unavailable');
}
```

The queue size and concurrency are operator settings; see the [configuration reference](/docs/reference/configuration.md).

## Caching and ETags

Maps are content-addressed: the cache key is a hash of the whole request (after defaults are applied) plus the renderer and data versions. The same request always gives the same image until the server's data or renderer changes.

- Image responses carry `ETag` (the hash), `Cache-Control: public, max-age=86400` and `X-Cache: HIT|MISS`. Send `If-None-Match` with the ETag to get an empty `304` when nothing changed.
- GET responses can sit behind a CDN or the browser cache as they are. Because the key covers every parameter, a changed parameter is a new URL and a new image; there is nothing to purge.
- `POST /v1/map?response=json` returns `{url, key, meta}`. The `url` (`/v1/images/{key}`) is immutable (`max-age=31536000, immutable`), so store it or put it in a page. If the cached image has expired, that URL returns `404 not-found`; re-send the POST to render it again.
- Two headers describe the result: `X-Image-Width` and `X-Image-Height` (the real pixel size, which can exceed `w × scale` with [neighbor `extend`](/docs/guides/neighbors.md#when-a-label-does-not-fit)) and `X-Render-Ms` on a fresh render.

Parameters beginning with an underscore (`&_v=2`) are ignored by the parser. Use one if you need a different URL for the same map, for example to get past a stale browser or CDN entry; the server still returns the same cached image.
