Errors
Every API error has the same shape. Branch on its code, never on its message.
The shape
A success is {"data": …}. A failure is {"error": …}, even for a mistyped path (404 NOT_FOUND) or a wrong method (405 METHOD_NOT_ALLOWED).
{
"error": {
"code": "VALIDATION_ERROR",
"message": "…written for a person; may change",
"details": { "field": "name" }
}
}Every answer carries an X-Request-ID header. Quote it when you report a problem. On a self-hosted installation, it is the request_id in the logs.
code- A stable name in capitals, such as
CONCURRENCY_LIMIT. Branch on it. message- One sentence for a person. It uses the installation’s own words and may change.
details- An object, on some errors: what a program can act on, such as the wrong
field, the missingpermission, theswitchthat is off or why a token ended (reason). Its keys depend on the code.
Branch on the code
Decide by status first, then by code. Retry only what says to retry, and wait as long as Retry-After asks when it is sent.
const res = await fetch(url, { method: "POST", headers, body });
const answer = await res.json();
if (!res.ok) {
const { code, message } = answer.error;
const retryAfter = res.headers.get("Retry-After");
if (retryAfter) {
// a 429 or 503 that says when: try again after that many seconds
} else if (res.status === 429) {
// CONCURRENCY_LIMIT or RUNS_WAITING_LIMIT: try again when a run finishes
} else if (code === "PAUSED") {
// a self-hosted installation without a license: don't retry
} else if (code === "CONFLICT") {
// publish the workflow first, or wait for the other request
} else {
throw new Error(`${code}: ${message}`); // fix the request
}
}res = requests.post(url, headers=headers, json=body)
if not res.ok:
error = res.json()["error"]
retry_after = res.headers.get("Retry-After")
if retry_after:
pass # a 429 or 503 that says when: try again after that many seconds
elif res.status_code == 429:
pass # CONCURRENCY_LIMIT or RUNS_WAITING_LIMIT: try when a run finishes
elif error["code"] == "PAUSED":
raise RuntimeError("Load a license first") # don't retry
elif error["code"] == "CONFLICT":
pass # publish the workflow first, or wait
else:
raise RuntimeError(f"{error['code']}: {error['message']}")What to do, by status
| Status | What happened | What to do |
|---|---|---|
| 400, 422 | The request or the workflow is not valid. For a workflow, Check a definition lists every problem and what is allowed. | Fix the request |
| 401 | The token is missing, expired or revoked. | Make a new token |
| 403 | The token may not do this, or the account is suspended. | Check its permissions |
| 404 | Nothing with that ID in the token’s workspace. | Check the ID |
| 405 | The path exists, but not with this method. Allow lists the ones it takes. | Use another method |
| 409 | The thing is in the wrong state, such as not published. Or someone saved the workflow since you read it (expectedRev). | Change its state, or read it again |
| 413 | The body is over 5 MiB (5,242,880 bytes). Malformed JSON is 400, not 413. | Send less |
| 415 | INVALID_: a Content-Type other than application/json. | Send JSON |
| 429 | A limit was reached. | Retry after Retry-After. CONCURRENCY_LIMIT and RUNS_ send none: retry when a run finishes. |
| 500 | INTERNAL_ERROR: something failed on GoRunner’s side. | Retry. If it repeats, send us its X-Request-ID |
| 503 | GoRunner couldn’t do it just now, the server is holding as much request data as it can (BUSY), or the installation is paused. Nothing was done. | Retry after Retry-After. PAUSED sends none: load a license. |
A 503 SERVICE_UNAVAILABLE means the database or Redis didn’t answer, so your token and the address weren’t checked. Only a token or an address that really isn’t there answers 401 or 404.
Error codes lists all 75 codes, with the calls that send each one.
When an installation is paused
A self-hosted installation runs only while a valid, unexpired license is loaded. Without one, a call with an API token made in GoRunner answers 503 PAUSED, and nothing is read, written or run. A token made inside a partner’s product answers 403 EMBED_NOT_AVAILABLE instead.
Calls work again within a second of loading a license, or within a minute if Redis is down. GoRunner Cloud never pauses.