ConceptsErrors
Errors
Every REST error has the same JSON shape. Branch on the numeric code, not on the message or the HTTP status alone; a missing permission answers 400, not 403.
Answer
REST errors come back as { success: false, http_status, code, name, message }. code and name identify the error; message explains it to a person. Validation errors, HTTP 422 with code 40000, add the record and the fields that failed. A missing permission answers 400 with code 50, never 403, so branch on code rather than on the status alone.
When a request fails, the status line says roughly what went wrong and the body says exactly what. Read the body.
The error envelope
Asking for more than 100 records per page is refused with HTTP 400:
{
"code": 117,
"name": "ERROR_PARAMETER_INVALID",
"message": "Limit must not be greater than 100",
"http_status": 400,
"success": false
}{
"errors": [
{
"message": "On attribute id: Invalid attribute value \"not-a-uuid\"",
"locations": [
{
"line": 1,
"column": 24
}
],
"path": [
"entities",
"account",
"getById"
],
"api_error": {
"code": 117,
"name": "ERROR_PARAMETER_INVALID",
"message": "On attribute id: Invalid attribute value \"not-a-uuid\""
}
}
],
"data": {
"entities": {
"account": {
"getById": null
}
}
}
}Show all 30 linesShow less
| Field | Holds |
|---|---|
success |
false |
http_ |
The HTTP status, repeated |
code |
The error's number, such as 117 |
name |
The error's name, such as ERROR_ |
message |
A sentence for people. Validation messages start with a newline: trim it before you show one. |
Permission errors also carry perm, the permission that is missing. Grant the permission as
permissions and scopes
describes.
Validation errors
HTTP 422 with code 40000 ERROR_ENTITY_VALIDATION adds what failed: entity_name, entity_id
and entity_index, then entity_errors for the record as a whole and field_errors for single
fields (each with code 40002 and its own errors), and step_checklist_errors. Errors of related
records nest the same way. Here an account is created with an owner that does not exist:
{
"code": 40000,
"name": "ERROR_ENTITY_VALIDATION",
"message": "\nRelated Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
"entity_id": "0a000000-0000-4000-a000-000000000001",
"entity_name": "Account",
"entity_errors": [],
"field_errors": [
{
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "owner_id",
"name": "ERROR_FIELD_VALIDATION",
"code": 40002,
"errors": [
{
"code": 120,
"name": "ERROR_RELATIONS_DOESNT_EXIST",
"message": "Related Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "owner_id"
}
]
}
],
"step_checklist_errors": [],
"entity_index": null,
"http_status": 422,
"success": false
}Show all 29 linesShow less
{
"errors": [
{
"message": "\nRelated Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createAccount"
],
"api_error": {
"code": 40000,
"name": "ERROR_ENTITY_VALIDATION",
"message": "\nRelated Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
"entity_id": "0a000000-0000-4000-a000-000000000001",
"entity_name": "Account",
"entity_errors": [],
"field_errors": [
{
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "owner_id",
"name": "ERROR_FIELD_VALIDATION",
"code": 40002,
"errors": [
{
"code": 120,
"name": "ERROR_RELATIONS_DOESNT_EXIST",
"message": "Related Client with id 00000000-0000-4000-a000-000000000001 doesn't exists.",
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "owner_id"
}
]
}
],
"step_checklist_errors": [],
"entity_index": null
}
}
],
"data": {
"createAccount": null
}
}Show all 46 linesShow less
In a batch sent with the default rollback, entity_index is null: the error does not say which
item failed (batch operations).
HTTP statuses
| HTTP | When | Typical codes |
|---|---|---|
| 200 | Success, including a batch with failed items sent with rollback-method=1 |
See batch operations |
| 201 | A record was created | |
| 202 | A write was queued for later with the delay parameter |
|
| 400 | A bad or unknown parameter, input the API cannot store, a missing permission, conflicting data | 117, 118, 50, 52, 2002 |
| 401 | Authentication failed: see authentication | ERROR_ |
| 404 | No such record, or no such entity. By default, a batch-delete with an unknown ID gets 404 for the whole call. |
143, 2006 |
| 422 | Validation failed, or a required value is missing. By default, a batch with a failed item gets 422 for the whole call, and nothing is written. | 40000, 119 |
| 429 | Too many requests: wait for Retry-After, see rate limits |
182 |
| 500 | Unexpected server error | 1000 |
A method a path does not have, such as PATCH /entities/{id} without the entity name, answers
HTTP 405 with a body of another shape, { "status": 405, "message": "…" }.
The REST API never answers 403 or 409
A missing permission is HTTP 400 with code 50 ERROR_NOT_ENOUGH_PERMISSIONS, and a revision or
data conflict is HTTP 400 too, code 2002 ERROR_CONFLICTING_DATA. Check code, not just the
status.
GraphQL errors
GraphQL answers HTTP 200 for a request it could run, with the failures in errors. When the API
refused part of it, errors[].api_error carries the same body as a REST error, without
success and http_status: code 117 for an ID that is not a UUID, and code 40000 with
field_errors for a mutation that fails validation. Pick GraphQL in the two responses above to
see them.
A query that does not match the schema, such as a field name with a typo, answers HTTP 400 with
only errors[].message, and so does an operation over the
GraphQL operation limits. Reading a
record that does not exist with getById returns null, not an error.
Handle errors in code
Raise an error that carries code, and decide on it: retry what can be retried, report the rest.
This helper, used on every request, also waits and retries on HTTP 429:
# call METHOD PATH [BODY]: one REST request; prints the body. On HTTP 429 it waits as long as
# Retry-After says (5 seconds when the header is missing) and tries again, three attempts in
# all. On any other error it prints "HTTP <status>, code <code> <name>: <message>" and the
# body, sets ERROR_CODE to the code and returns 1.
call() {
local attempt headers response status delay
local args=(-X "$1" "$COEVERA_BASE$2" -u "$COEVERA_API_KEY:$COEVERA_API_SECRET")
if [ $# -ge 3 ]; then args+=(-H "Content-Type: application/json" -d "$3"); fi
headers=$(mktemp)
for attempt in 1 2 3; do
response=$(curl -sS -D "$headers" -w '\n%{http_code}' "${args[@]}")
status=${response##*$'\n'}
response=${response%$'\n'*}
if [ "$status" = 429 ] && [ "$attempt" -lt 3 ]; then
delay=$(sed -n 's/^[Rr]etry-[Aa]fter: *\([0-9][0-9]*\).*/\1/p' "$headers")
sleep "${delay:-5}"
continue
fi
rm -f "$headers"
if [ "$status" -ge 400 ]; then
# The message is for people; branch on the numeric code.
jq -r --arg status "$status" \
'"HTTP \($status), code \(.code) \(.name): \(.message | sub("^\\s+"; "") | sub("\\s+$"; ""))"' <<<"$response"
echo "$response"
ERROR_CODE=$(jq -r '.code' <<<"$response")
return 1
fi
echo "$response"
return 0
done
}// An error the API answered: the HTTP status, and the code, name and message of its body.
class ApiError extends Error {
constructor(
readonly status: number,
readonly code: number,
readonly errorName: string,
message: string,
readonly body: unknown,
) {
super(`HTTP ${status}, code ${code} ${errorName}: ${message}`);
}
}
const sleep = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
// call: one REST request. On HTTP 429 it waits as long as Retry-After says (5 seconds when the
// header is missing) and tries again, three attempts in all. Any other error is an ApiError.
async function call(path: string, init: { method?: string; body?: unknown } = {}) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(`${base}${path}`, {
method: init.method ?? "GET",
headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
body: init.body === undefined ? undefined : JSON.stringify(init.body),
});
if (res.status === 429 && attempt < 3) {
await sleep(Number(res.headers.get("Retry-After")) || 5);
continue;
}
const body = await res.json();
// The message is for people; branch on the numeric code.
if (!res.ok) throw new ApiError(res.status, body.code, body.name, String(body.message).trim(), body);
return body;
}
}class ApiError(Exception):
"""An error the API answered: the HTTP status, and the code, name and message of its body."""
def __init__(self, status, body):
self.status, self.body = status, body
self.code, self.name = body.get("code"), body.get("name")
message = str(body.get("message")).strip()
super().__init__(f"HTTP {status}, code {self.code} {self.name}: {message}")
def call(method, path, **kwargs):
"""One REST request. On HTTP 429 it waits as long as Retry-After says (5 seconds when the
header is missing) and tries again, three attempts in all. Any other error is an ApiError."""
for attempt in range(1, 4):
response = session.request(method, f"{BASE}{path}", timeout=30, **kwargs)
if response.status_code == 429 and attempt < 3:
time.sleep(float(response.headers.get("Retry-After") or 5))
continue
body = response.json()
# The message is for people; branch on the numeric code.
if not response.ok:
raise ApiError(response.status_code, body)
return body# call REQUEST: POST one GraphQL request (a JSON body); prints the data. On HTTP 429 it waits as
# long as Retry-After says (5 seconds when the header is missing) and tries again, three
# attempts in all. On any error it prints "HTTP <status>, code <code> <name>: <message>" and
# the error, sets ERROR_CODE to the api_error code (null when there is none) and returns 1.
call() {
local attempt headers response status delay
headers=$(mktemp)
for attempt in 1 2 3; do
response=$(curl -sS -D "$headers" -w '\n%{http_code}' "$COEVERA_GRAPHQL" \
-u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
-H "Content-Type: application/json" \
-d "$1")
status=${response##*$'\n'}
response=${response%$'\n'*}
if [ "$status" = 429 ] && [ "$attempt" -lt 3 ]; then
delay=$(sed -n 's/^[Rr]etry-[Aa]fter: *\([0-9][0-9]*\).*/\1/p' "$headers")
sleep "${delay:-5}"
continue
fi
rm -f "$headers"
if [ "$status" != 200 ] || jq -e '.errors' <<<"$response" >/dev/null; then
# The message is for people; branch on the numeric code.
jq -r --arg status "$status" '.errors[0]
| "HTTP \($status), code \(.api_error.code) \(.api_error.name // ""): \(.message | sub("^\\s+"; "") | sub("\\s+$"; ""))"' <<<"$response"
jq '.errors[0]' <<<"$response"
ERROR_CODE=$(jq -r '.errors[0].api_error.code' <<<"$response")
return 1
fi
jq '.data' <<<"$response"
return 0
done
}// An error the API answered: the HTTP status, and the code and name of api_error when the
// error has one (a document the schema refuses has none).
class ApiError extends Error {
constructor(
readonly status: number,
readonly code: number | undefined,
readonly errorName: string | undefined,
message: string,
readonly error: unknown,
) {
super(`HTTP ${status}, code ${code ?? "none"} ${errorName ?? ""}: ${message}`);
}
}
const sleep = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
// call: one GraphQL request. On HTTP 429 it waits as long as Retry-After says (5 seconds when
// the header is missing) and tries again, three attempts in all. Any error is an ApiError.
async function call(query: string, variables: object = {}) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(endpoint, {
method: "POST",
headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
if (res.status === 429 && attempt < 3) {
await sleep(Number(res.headers.get("Retry-After")) || 5);
continue;
}
const body = await res.json();
const error = body.errors?.[0];
// The message is for people; branch on the numeric code.
if (!res.ok || error) {
throw new ApiError(res.status, error?.api_error?.code, error?.api_error?.name, String(error?.message).trim(), error);
}
return body.data;
}
}class ApiError(Exception):
"""An error the API answered: the HTTP status, and the code and name of api_error when the
error has one (a document the schema refuses has none)."""
def __init__(self, status, code, name, message, error):
super().__init__(f"HTTP {status}, code {code} {name}: {message}")
self.status, self.code, self.name, self.error = status, code, name, error
def call(query, variables=None):
"""One GraphQL request. On HTTP 429 it waits as long as Retry-After says (5 seconds when the
header is missing) and tries again, three attempts in all. Any error is an ApiError."""
for attempt in range(1, 4):
response = session.post(ENDPOINT, json={"query": query, "variables": variables or {}}, timeout=30)
if response.status_code == 429 and attempt < 3:
time.sleep(float(response.headers.get("Retry-After") or 5))
continue
body = response.json()
error = (body.get("errors") or [None])[0]
# The message is for people; branch on the numeric code.
if not response.ok or error:
api_error = (error or {}).get("api_error") or {}
message = str((error or {}).get("message")).strip()
raise ApiError(response.status_code, api_error.get("code"), api_error.get("name"), message, error)
return body["data"]Then decide on the code. A validation error is not worth retrying: report the field that failed.
# An owner that does not exist: HTTP 422, code 40000. field_errors names the field that failed.
# Nothing is written.
ACCOUNT='{ "name": "Northwind Analytics", "owner_id": "00000000-0000-4000-a000-000000000000" }'
if ! call POST "/entities/Accounts" "$ACCOUNT" && [ "$ERROR_CODE" != 40000 ]; then
exit 1
fi// An owner that does not exist: HTTP 422, code 40000. field_errors names the field that failed.
// Nothing is written.
try {
await call("/entities/Accounts", {
method: "POST",
body: { name: "Northwind Analytics", owner_id: "00000000-0000-4000-a000-000000000000" },
});
} catch (err) {
if (!(err instanceof ApiError) || err.code !== 40000) throw err;
console.log(err.message, JSON.stringify(err.body, null, 2));
}# An owner that does not exist: HTTP 422, code 40000. field_errors names the field that failed.
# Nothing is written.
try:
call(
"POST",
"/entities/Accounts",
json={"name": "Northwind Analytics", "owner_id": "00000000-0000-4000-a000-000000000000"},
)
except ApiError as err:
if err.code != 40000:
raise
print(err, json.dumps(err.body, indent=2))# An owner that does not exist: HTTP 200 with errors, api_error code 40000. Its field_errors
# name the field that failed. Nothing is written.
CREATE=$(jq -n '{
query: "mutation Create($input: CreateAccountInput!) { createAccount(input: $input) { account { id } } }",
variables: { input: { name: "Northwind Analytics", ownerId: "00000000-0000-4000-a000-000000000000" } }
}')
if ! call "$CREATE" && [ "$ERROR_CODE" != 40000 ]; then
exit 1
fi// An owner that does not exist: HTTP 200 with errors, api_error code 40000. Its field_errors
// name the field that failed. Nothing is written.
try {
await call(
`mutation Create($input: CreateAccountInput!) {
createAccount(input: $input) { account { id } }
}`,
{ input: { name: "Northwind Analytics", ownerId: "00000000-0000-4000-a000-000000000000" } },
);
} catch (err) {
if (!(err instanceof ApiError) || err.code !== 40000) throw err;
console.log(err.message, JSON.stringify(err.error, null, 2));
}# An owner that does not exist: HTTP 200 with errors, api_error code 40000. Its field_errors
# name the field that failed. Nothing is written.
try:
call(
"""
mutation Create($input: CreateAccountInput!) {
createAccount(input: $input) { account { id } }
}
""",
{"input": {"name": "Northwind Analytics", "ownerId": "00000000-0000-4000-a000-000000000000"}},
)
except ApiError as err:
if err.code != 40000:
raise
print(err, json.dumps(err.error, indent=2))Codes you meet most
The error-code reference lists every code of the current release with its name and HTTP status. These are the ones integrations meet most:
| Code | Name | HTTP | Meaning |
|---|---|---|---|
| 50 | ERROR_ |
400 | The application lacks a permission |
| 117 | ERROR_ |
400 | A parameter or value is invalid; also a repeated query key, a page size over 100 and input the API cannot store |
| 118 | ERROR_ |
400 | A parameter the endpoint does not take |
| 119 | ERROR_ |
422 | A required value is missing |
| 120 | ERROR_ |
422 | A related record does not exist, inside field_ |
| 143 | ERROR_ |
404 | No record with that ID |
| 182 | ERROR_ |
429 | Too many requests; see Retry-After |
| 1000 | ERROR_ |
500 | Unexpected server error |
| 2002 | ERROR_ |
400 | The data conflicts, for example an id in the body that differs from the one in the URL |
| 2006 | ERROR_ |
404 | No entity with that name in the path |
| 40000 | ERROR_ |
422 | A record failed validation; see entity_ and field_ |
| 40002 | ERROR_ |
422 | One field failed validation, inside field_ |
Limits
| Limit | Details |
|---|---|
| 403 and 409 | Never used; permission problems and conflicts are 400 |
message |
Written for people; match on code |
| Input the API cannot store | NaN or Infinity, a NUL character (\u0000), an unpaired surrogate, a value too long to index or input nested too deeply: HTTP 400, code 117, with a message that names the problem |