AshOnetime.Error is a Splode error of class :invalid, so
Ash recognizes it and preserves it through the action pipeline. When a protected action
fails, the typed :code reaches the caller — either as the leaf error directly (single
error) or inside Ash.Error.Invalid{errors: [...]} (multiple errors) — instead of being
wrapped as an unknown error.
Reading the code
AshOnetime.Error.code/1 recovers the typed code from whatever Ash hands the caller,
without pattern-matching the wrapper shape:
case Ash.create(changeset) do
{:ok, record} -> record
{:error, error} ->
case AshOnetime.Error.code(error) do
:nonce_already_used -> {:conflict, "nonce was already used"}
:key_reused_with_different_request -> {:conflict, "key reused with a different request"}
:request_in_progress -> {:conflict, "request is already processing"} # 425 Too Early also fits
:verification_failed -> {:unauthorized, "verification failed"}
:verification_timeout -> {:service_unavailable, "verification timed out"}
nil -> {:internal_server_error, "unexpected error"} # not an ash_onetime error
end
endcode/1 returns nil for any value that is not an AshOnetime.Error and contains no
AshOnetime.Error leaf — so "ash_onetime rejected this with a known code" is cleanly
distinguishable from "some other error occurred."
Class and HTTP
All AshOnetime.Error codes are class :invalid. AshJsonApi and AshGraphql auto-map class
:invalid to the 4xx family. That default is correct for the client-input codes below, but a
family of server-fault and transport codes overrides it to 5xx — a consumer mapping
class→HTTP must special-case those (the two 5xx tables below), or a store outage, a trusted
clock fault, or an internal invariant violation is mis-reported to the client as a 4xx.
This page lists every code a caller can observe from AshOnetime.Error.code/1, including the
token-verification codes, the trusted-clock codes, and the store-fault/transport codes routed
through the authoritative store — not only the codes raised on the happy admission path.
Client-input / operational codes (4xx)
| Code | HTTP | Meaning |
|---|---|---|
:nonce_already_used | 409 | A one-time nonce was already spent. |
:key_reused_with_different_request | 409/422 | An idempotency key was reused with a different request fingerprint. |
:request_in_progress | 409 / 425 | A processing claim is still in flight for this key. |
:verification_failed | 401 | A trusted verifier rejected the token. |
:verification_timeout | 503 | A trusted verifier timed out (retryable). |
:fingerprint_too_large | 422 | The request fingerprint exceeded its byte limit. |
:fingerprint_unavailable | 422 | The request fingerprint could not be computed. |
:key_too_large | 422 | A key component exceeded its byte limit. |
:key_unavailable | 422 | A key component could not be resolved. |
:key_resolution_failed | 422 | The key resolver callback failed. |
:key_not_found | 404 | A referenced key was not found. |
:scope_unavailable | 422 | A scope component could not be resolved. |
:invalid_key | 422 | A key is structurally invalid. |
:invalid_key_role | 422 | A key source role is unrecognized. |
:invalid_window | 422 | A nonce window is malformed. |
:invalid_nonce_window | 422 | The authoritative store rejected a malformed nonce window. |
:invalid_expires_at | 422 | A verified expiry is malformed. |
:invalid_token | 422 | A token is structurally invalid. |
:malformed_token | 422 | A token envelope could not be parsed. |
:invalid_key_id | 422 | A token key id is missing or out of bounds. |
:invalid_namespace | 422 | A token namespace is missing or out of bounds. |
:invalid_issued_at | 422 | A token issuance timestamp is malformed. |
:invalid_trust_boundary | 422 | HMAC material did not prove same-service trust. |
:invalid_encoding | 422 | A canonical encoding is invalid. |
:noncanonical_encoding | 422 | A canonical encoding is non-canonical. |
:noncanonical_envelope | 422 | A token envelope is non-canonical. |
:invalid_signature | 422 | A token signature is invalid. |
:signing_failed | 422 | A token could not be signed. |
:invalid_message | 422 | A signer message is invalid. |
:algorithm_mismatch | 422 | A token algorithm does not match. |
:unsupported_algorithm | 422 | A token algorithm is not supported. |
:namespace_mismatch | 422 | A token namespace does not match. |
:token_too_large | 422 | A token exceeded its byte limit. |
:duplicate_field | 422 | A canonical map carried a duplicate field. |
:duplicate_map_key | 422 | A canonical map carried a duplicate key. |
:unsupported_term | 422 | A canonical term is unsupported. |
:limit_exceeded | 422 | A configured limit was exceeded. |
:missing_option | 422 | A required DSL option is missing. |
:invalid_option | 422 | A DSL option is invalid. |
:invalid_options | 422 | DSL options are invalid. |
:reserved_verification_input | 422 | Reserved verification input was supplied. |
:response_rejected | 422 | The response classifier rejected the result. |
:response_rollback | 422 | The response classifier requested a rollback. |
:response_fields_invalid | 422 | The response field allowlist is invalid. |
:response_value_invalid | 422 | The response value is invalid. |
:response_codec_mismatch | 422 | The persisted response codec does not match. |
:response_contract_mismatch | 422 | The persisted response contract does not match. |
:external_effect_unavailable | 422 | An external effect is unavailable. |
:external_recovery_unavailable | 422 | External recovery is unavailable. |
Server-fault codes (5xx — override the class default)
These are NOT client input. A consumer mapping class→HTTP to a blanket 4xx would
mis-categorize them; the per-code HTTP below overrides the :invalid class.
| Code | HTTP | Meaning |
|---|---|---|
:store_invariant | 500 | The authoritative store returned a result that violated an internal invariant (integrity fault, not client input). |
:outcome_unknown | 503 | External recovery was ambiguous; the effect's outcome could not be determined (retryable). |
:invalid_evaluated_at | 500 | The trusted evaluation clock returned an invalid time. |
:response_payload_invalid | 500 | The persisted response payload is invalid. |
:response_persisted_state_invalid | 500 | The persisted response state is invalid. |
:response_digest_mismatch | 500 | The persisted response digest does not match the payload. |
:response_classifier_failed | 500 | The response classifier callback raised. |
:response_classifier_invalid | 500 | The response classifier returned an invalid disposition. |
:response_codec_failed | 500 | The response codec raised. |
:response_codec_invalid | 500 | The response codec output is invalid. |
:response_contract_invalid | 500 | The response contract is invalid. |
:response_completion_failed | 500 | Response completion failed. |
:admission_request_invalid | 500 | The admission request is internally invalid. |
:admission_unavailable | 503 | Admission is unavailable (fail-closed; retryable). |
:telemetry_invalid | 500 | A telemetry event was invalid (internal). |
Store-fault and transport codes (5xx — override the class default)
When the authoritative PostgreSQL store is unavailable or reports a fault, admission fails
closed (nonces always; idempotency unless untracked execution is explicitly enabled) and the
store's reason surfaces verbatim through AshOnetime.Error.code/1. A generic
:store_failure covers any reason not enumerated below. Treat the 503 codes as retryable
(the request may succeed on retry once the store recovers) and the 500 codes as integrity or
configuration faults that will not clear by retrying.
| Code | HTTP | Meaning |
|---|---|---|
:checkout_unavailable | 503 | No database connection could be checked out (pool exhausted or down). |
:disconnected | 503 | The database connection dropped mid-operation. |
:lock_timeout | 503 | A row lock could not be acquired within the timeout. |
:dispatched_unknown | 503 | A statement was dispatched but its outcome is unknown (retryable). |
:store_failure | 503 | The authoritative store failed for an unenumerated reason. |
:missing_prefix | 500 | The context-tenant schema prefix is missing or out of the 1..63-byte bound (fail-closed, not truncated). |
:not_in_transaction | 500 | Admission ran outside the required database transaction. |
:unsupported_isolation | 500 | The connection's transaction isolation level is unsupported. |
:corrupt_payload | 500 | A persisted response payload failed its integrity check. |
:invalid_request | 500 | The store received an internally malformed request. |
details and classified data
The details map is part of the error struct and reaches sinks that serialize or inspect
the struct — inspect(error) in logs is the present-tense sink, and any future
AshJsonApi/AshGraphql integration renders struct fields into HTTP/API responses. (Note:
Exception.message/1 and Ash.Error.error_descriptions/1 render only the message field,
not details — but details is still on the struct and should be treated as
renderable-to-callers.) Never put key material, tokens, payloads, signatures, or PII in
details.
The library's own call sites populate details only with non-secret context — field or
option names (atoms) and numeric byte limits (e.g. %{field: :key_id, maximum: 128}).
One path carries application-supplied data: when an application's key resolver returns
{:error, reason}, ash_onetime wraps it as
Error.new(:key_resolution_failed, ..., %{reason: reason}) (lib/ash_onetime/token.ex). A
resolver that returns key material in its error reason would leak it through the rendered
error. Keep resolver error reasons secret-free — return an atom or a generic message, not
the key. (Audited at this release; re-audit any future call site that adds details.)