FerrisKeyDocs

API errors

The API answers a failed request in one of three shapes. Which one you get depends on the endpoint and on the kind of failure, and each carries a machine-readable code that is meant to be matched on. The human-readable text is not.

ShapeWhereMachine-readable field
StandardMost endpointsreason
ValidationA request body or a value that fails validation, status 422errors[].code
OAuthThe token endpoint, the device flow and some failures of the authorization requesterror

The standard shape

{
  "code": "E_NOT_FOUND",
  "status": 404,
  "reason": "not_found",
  "message": "Resource not found"
}
FieldMeaning
codeOne value per status: E_BAD_REQUEST, E_UNAUTHORIZED, E_FORBIDDEN, E_NOT_FOUND, E_CONFLICT, E_INTERNAL_SERVER_ERROR or E_SERVICE_UNAVAILABLE
statusThe HTTP status, repeated in the body
reasonA stable snake_case code for the specific failure. When an error has no more specific one, it repeats the class: bad_request, unauthorized, forbidden, not_found, conflict, internal_server_error or service_unavailable
messageEnglish text for a developer

Some reasons you will meet:

StatusreasonWhen
400invalid_queryA listing parameter is unknown or out of range. See Paginated listings
400unexpected_payloadThe request body is not valid JSON, or does not match the shape the endpoint expects
400email_already_existsAnother user of the realm already has that email address
401token_not_foundThe request has no bearer token
401invalid_credentialsThe username or the password is wrong
401account_lockedToo many failed attempts. See Users
403forbiddenThe caller lacks a permission the operation needs
403user_disabledThe account is disabled
403elevation_requiredA sensitive account operation needs a fresh re-authentication. See Account Security
404not_foundThe resource does not exist, or it belongs to another realm
409user_id_already_existsA user is created with an id that is already taken
409password_credential_already_existsA password hash is imported for a user who already has a password
409last_sign_in_meansRemoving a credential would leave the account with no way to sign in
500internal_server_errorSomething failed on the server. The message starts with Internal Server Error: and can carry internal detail, so do not show it to end users
503client_under_maintenanceThe client is in maintenance and refuses sign-ins

The list is long, because every domain error has its own reason. These are examples, not the whole set.

Validation errors

A body that fails validation, a password that breaks the realm’s policy, a hash the server cannot accept and a locale that is not well formed all answer with 422 and a list. The list has one entry for each problem, so a form can show them all at once.

{
  "errors": [
    { "field": "username", "code": "length", "message": "username is required" }
  ]
}
FieldMeaning
fieldThe name of the field that failed
codeThe failed rule. For a body checked field by field it is the rule, such as length, email or range. For a password it is one of too_short, missing_uppercase, missing_lowercase, missing_number, missing_special, insufficient_entropy and common_password. Other validations use their own code, for example invalid_password_hash or malformed_locale
messageEnglish text for a developer

This shape has no reason and no status in the body. Read the status from the response.

OAuth errors

The token endpoint, the device flow and some failures of the authorization request, such as a missing PKCE challenge, answer in the shape RFC 6749 defines, so that any OAuth library can read them.

{
  "error": "invalid_client",
  "error_description": "Client authentication failed."
}
errorStatusMeaning
invalid_request400A required parameter is missing, or the request is malformed
invalid_grant400The code, refresh token, credentials or session is not valid. Also what the password grant answers when the account still owes a login step
unauthorized_client400The client may not use this grant
unsupported_grant_type400The grant_type is not one FerrisKey knows
invalid_scope400The requested scope is not allowed
invalid_client401Client authentication failed. The response carries WWW-Authenticate: Basic
temporarily_unavailable429 or 503Too many failed client authentications from one address (429, Retry-After: 60), or the server is at capacity (503, Retry-After: 1)

The device flow adds the codes from RFC 8628: authorization_pending, slow_down, expired_token and access_denied, all with status 400.

What to match on

Branch on the status for the class of failure, then on the code for the specific one. Do not branch on message: it is English prose, its wording is not part of the contract, and some messages carry detail that changes from one request to the next.

One line reads the code out of any of the three shapes:

const body = await response.json()
const code = body.reason ?? body.error ?? body.errors?.[0]?.code

The console does the same. It picks the text it shows from reason, from error for an OAuth response, or from code for each entry of a validation error, in the language of the person using it, and falls back on message only when it has no text for the code. See Localization.