Features Apograph CMS on GitHub

Errors and status codes

Content plugin @apograph/content-server@apograph/content-admin

What each status means, and where two different causes answer identically on purpose.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

Status codes

StatusWhen
200A successful read or write.
201An entry or asset was created.
204A delete succeeded.
400Malformed input — a bad filter, an unknown fields name, an unknown locale, a missing workspace header on a multi-workspace token.
401Authentication failed. Always bare.
403Authenticated, but not permitted — a read token asking for drafts, or a workspace outside the token’s bucket.
404Not found, or not visible to this caller.
409A conflict — most often a duplicate (translation group, locale).
413The request body exceeded MAX_REQUEST_BODY, or an upload exceeded its cap.
422The input was well-formed but failed validation.
429Rate limited. Applies to login, not to content reads.

Validation failures

A 422 carries a list of issues, each naming a field:

{
    "statusCode": 422,
    "message": [
        { "field": "title", "message": "must be at most 200 characters" },
        { "field": "author", "message": "must reference an existing author" }
    ]
}

The same shape comes back from a save, from a publish that fails the gate, and from a failed item inside a bulk save.

Where two causes answer identically

Several distinctions are deliberately collapsed. They are not bugs, and reading them as bugs will send you looking for a fix that should not exist.

The answerThe causes it covers
One bare 401No header, wrong scheme, unknown token, revoked token, expired token
404The entry does not exist, is in another workspace, is soft-deleted, or is unpublished and you hold a read token
422 on a relation or media targetThe target does not exist, is in another workspace, or is disallowed by accept

In each case, answering differently would confirm whether an id you cannot otherwise see is real. That is an enumeration signal, and the API gives it up everywhere it can.

Silent successes worth knowing about

Two things return success while doing less than you might assume.

An unknown field name in a write is dropped. Not stored, not rejected, 200 returned. A misspelt field writes nothing and says nothing. Read back what you wrote when working against the API by hand.

A bulk save is always 200. Items are written one transaction at a time, so the batch cannot roll back as a unit. The response carries a verdict per item in request order; a failing item reports the status and message the single-entry call would have returned. Check the verdicts — the HTTP status will not tell you.

GraphQL errors

GraphQL answers 200 with an errors array, whatever went wrong. The status the equivalent REST call would have returned is in extensions.status:

{
    "data": null,
    "errors": [
        {
            "message": "…",
            "extensions": { "status": 422, "issues": [] }
        }
    ]
}

This is the one thing a consumer porting from REST has to adjust to. Do not branch on the HTTP status over GraphQL — branch on extensions.status.

An ungranted type over GraphQL is a validation error rather than a 404, because the field genuinely does not exist in your schema.

Cost-limit rejections

A GraphQL document over the depth, field-count, complexity or length budget is refused before execution, with an error naming the limit. See GraphQL.

Rate limiting

Login is rate limited. Content reads and writes are not — a token that hammers the API is bounded by page size and the cost limits, not by a request counter.

If you need one, it belongs in the reverse proxy in front of Apograph. See configuration for TRUST_PROXY, which you must set correctly for any of this to attribute requests to the right client.