Errors and status codes
The error envelope every route uses, what each status means on the storefront routes, and the zone answers that carry no body.
The error envelope
Every error, from every route, has the same JSON body:
{
"code": 401,
"error": "UNAUTHORIZED",
"message": "Unauthorized",
"method": "GET",
"path": "/search/query/linen",
"timestamp": "2026-11-09T01:02:03.456Z"
}
code repeats the HTTP status, error is its name (BAD_REQUEST, NOT_FOUND...), and message says
what went wrong when the server knows. Anything unexpected is a 500 in the same envelope. Branch on the
status code, not on message: messages are for people and can change.
Status codes on the storefront routes
| Status | When |
|---|---|
200 | Success. Also some deliberate empty answers, below. |
201 | Success for a zone POST (the cart zone route). |
400 | A bad request: a negative pageNumber, an empty collection handle, or a search term of the string undefined. |
401 | No credential, or an unknown bypass token or origin. See authentication. |
404 | An unknown collection (Collection not found), or a path that isn't a route. (An unknown product handle is a null in the products array, not a 404.) |
406 | The search engine rejected the query, usually because of a malformed filter. |
500 | A server fault. Retry once with backoff, then give up gracefully. |
Successful responses with no body
A zone whose matching rule group shows nothing answers 2xx with an empty body instead of JSON, so read the text before parsing:
const res = await fetch(url);
if (!res.ok) {
const error = await res.json().catch(() => null); // the envelope, when there is one
throw new Error(`Mercury ${res.status}: ${error?.message ?? res.statusText}`);
}
const text = await res.text();
const data = text ? JSON.parse(text) : null;
Resilience
A failing Mercury call should never break a storefront page. Render the theme's own content (or nothing) when a collection, zone or search call fails, and set a timeout: the storefront routes normally answer well under a second.