Errors
The closed set of codes, their HTTP statuses, and which are worth retrying.
Every non-2xx response is the same envelope, whatever the route:
{ "error": { "code": "...", "message": "...", "request_id": "...", "details": [], "quota": {} } }
Switch on code, never on message. The code is a closed set with a fixed status per
member; the message is written for a person and may be reworded in any release.
The codes
| Code | Status | Means | Retry? |
|---|---|---|---|
invalid_request |
400 | the request was malformed or a field was wrong. details names the fields |
no |
unauthenticated |
401 | no credential, or one the API does not accept | no |
quota_exceeded |
402 | a plan entitlement declined it. quota says which |
no |
permission_denied |
403 | authenticated, but the role or scope does not allow it | no |
not_found |
404 | no such object in this organization | no |
conflict |
409 | the current state forbids it — an illegal transition, a held lease, a reused idempotency key | no |
precondition_failed |
412 | something that had to be true first was not | no |
rate_limited |
429 | too many attempts; wait | yes |
internal_error |
500 | a fault on our side | yes |
unsupported_operation |
501 | the deployment is not configured for this feature | no |
compute_unavailable |
503 | the machine is not reachable yet, or the substrate is unwell | yes |
not_found rather than permission_denied for another organization’s object is deliberate: a
403 would confirm that the id exists.
Fields
| Field | Always? | What it is |
|---|---|---|
code |
yes | the closed set above |
message |
yes | human-readable, not machine-parseable |
request_id |
yes | quote it in a support request |
details |
on invalid_request |
{ field, message }, with field a dotted path into the body |
quota |
on quota_exceeded |
which limit declined this request |
Handling them
import { RasterError } from "@raster/sdk";
try {
await client.machines.create({ size: "large" });
} catch (error) {
if (!RasterError.is(error)) throw error;
switch (error.code) {
case "quota_exceeded":
// render this, not the message: it names the limit, the current value,
// the maximum, and where to go to lift it.
show(error.quota);
break;
case "invalid_request":
for (const d of error.details) markField(d.field, d.message);
break;
default:
report(error.code, error.requestId);
}
}
from raster import RasterError
try:
client.machines.create(size="large")
except RasterError as error:
if error.code == "invalid_request":
for d in error.details:
mark_field(d["field"], d["message"])
else:
report(error.code, error.request_id)
error.status is 0 on a transport failure — a timeout, a DNS failure, a proxy that answered
instead of the API — and request_id is null for the same reason. Anything the API itself
produced has both.
compute_unavailable is usually not a failure
A machine that is running but whose agent has not published its address yet answers every
desktop route with compute_unavailable. That happens on every boot. It is why both SDKs
treat the code as retryable and why waitForDesktop swallows it while polling.
Treat it as “not yet”, not as “broken”, and put a deadline on the waiting rather than a retry count.
Gateway close codes
A websocket failure is not this envelope — it is a close code. See
the realtime gateway. They map onto the same vocabulary:
4401 is unauthenticated, 4403 is permission_denied, 4404 is not_found, 4409 is
conflict, 4429 is rate_limited, and 4503 is compute_unavailable.