Reference
Errors
Status codes, the two different error body formats, and the one case where a failure arrives with a 200.
Error handling on this API has a wrinkle worth knowing about before you write your client. There are
two body formats, and one class of endpoint returns failures with a 200.
Two formats
Plain text — Content-Type: text/plain; charset=utf-8, with a trailing newline. Used by
authentication failures, method-not-allowed, and all manifest errors:
manifest: not found
JSON — Content-Type: application/json. Used by prepare-write, cap rejections, and rate
limiting:
{ "error": "redundancy 1.400 < 1.5" }
A robust client checks the content type, or simply tries to parse JSON and falls back to the raw text:
async function readError(res: Response): Promise<string> {
const text = await res.text()
try {
const parsed = JSON.parse(text)
return parsed.error ?? text
} catch {
return text.trim()
}
}
The 200-with-an-error case
Check the body, not only the status
Some endpoints write a JSON error object without setting a failure status, so a failed call arrives
as 200 OK with {"error": "..."} in the body. res.ok is therefore not a sufficient success
check. Always inspect the parsed body for an error key. This affects operational endpoints more than
the core object path, but a client that checks both is correct everywhere and costs nothing.
async function call(method: string, path: string, body?: unknown) {
const res = await signedFetch(method, path, body)
const text = await res.text()
if (!res.ok) throw new TesseraError(res.status, await readErrorFrom(text))
const parsed = text ? JSON.parse(text) : null
if (parsed && typeof parsed === 'object' && 'error' in parsed) {
throw new TesseraError(res.status, parsed.error) // 200, but not a success
}
return parsed
}
Status codes
| Status | Meaning | Retry? |
|---|---|---|
200 | Success — unless the body carries an error key. | — |
201 | Credential created. | — |
204 | Success, no body. Deletes and auth/check. | — |
400 | Malformed request: bad JSON, bad hex, invalid geometry, missing slab. | No — fix the request. |
401 | Authentication failed. | Only after re-signing. |
402 | Storage cap exceeded. | No — needs a cap change or a deletion. |
403 | The resource belongs to another account. | No. |
404 | Not found for this account. | No. |
405 | Wrong HTTP method for the path. | No. |
409 | Slab still referenced by an object. | After deleting the object. |
429 | Rate limited. | Yes, after Retry-After. |
500 | Server-side failure. | Yes, with backoff. |
503 | A subsystem is unavailable — often no providers right now. | Yes, with backoff. |
Manifest error strings
Manifest errors arrive as plain text with these exact bodies. Match on them if you need to branch:
| Body | Status | Meaning |
|---|---|---|
manifest: not found | 404 | No such slab or object for this account. |
manifest: forbidden for this account | 403 | It exists, but belongs to someone else. |
manifest: invalid argument | 400 | Malformed field. |
manifest: invalid erasure coding params | 400 | Geometry, duplicate provider, duplicate root, or key length. |
manifest: slab still referenced by an object | 409 | Unpin blocked by a live reference. |
manifest: empty account | 400 | No account resolved from the request. |
manifest: object references missing slab | 400 | Register the slab first. |
Authentication errors
All 401, all plain text. See
Authentication § failure modes for what each one indicates and
how to debug signature mismatch.
missing auth params · invalid validUntil · signature expired
invalid credential · unknown credential · invalid signature · signature mismatch
Rate limiting
HTTP/1.1 429 Too Many Requests
Retry-After: 5
{"error":"rate limited","retryAfter":5}
Honour Retry-After. Provisioning has its own limit with a distinct body:
{ "error": "rate limited: max 5 keys per hour" }
See Limits.
A retry policy that behaves
const RETRYABLE = new Set([429, 500, 502, 503, 504])
async function withRetry<T>(fn: () => Promise<T>, attempts = 5): Promise<T> {
let lastErr: unknown
for (let i = 0; i < attempts; i++) {
try {
return await fn()
} catch (err) {
lastErr = err
const status = (err as TesseraError).status
if (!RETRYABLE.has(status)) throw err // 4xx: retrying cannot help
const retryAfter = (err as TesseraError).retryAfter
const backoff = retryAfter
? retryAfter * 1000
: Math.min(2 ** i * 250, 8000) + Math.random() * 250 // jitter matters
await new Promise((r) => setTimeout(r, backoff))
}
}
throw lastErr
}
Two things this gets right that naive retry loops do not: it refuses to retry 4xx responses, where
the answer will not change, and it adds jitter, so a fleet of clients recovering from an outage does
not synchronise into a thundering herd.
Errors worth special handling
| Situation | Right response |
|---|---|
503 no contracted hosts available from prepare-write | Backoff and retry. Transient provider availability, not a client bug. |
| Shard write fails at a provider | Get a replacement provider from prepare-write; do not retry the same one indefinitely. |
402 storage cap exceeded | Stop, alert an operator. Never retry on a loop. |
401 signature expired | Re-sign with a fresh validUntil. If it recurs, check clock skew on your host. |
409 slab still referenced | Delete referencing objects, then prune. |
200 with an error key | Treat as the failure it is; log the endpoint, since this is the easiest failure to swallow accidentally. |