Skip to content

Latest commit

 

History

History
162 lines (143 loc) · 10.4 KB

File metadata and controls

162 lines (143 loc) · 10.4 KB

Errors and HTTP mapping

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
end

code/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 — including, on external-effect actions, a same-key retry refused by the pre-peer claim lock within its configured wait (ADR-0010).
: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.
:worker_timeout 503 The independently committed claim's worker exceeded its 30s ceiling.
: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.)