SDK

Errors

Every diagnos exception, what the SDK already retries, clock skew, and how errors map to CLI exit codes and HTTP statuses.

On this page

English · Português (Brasil)

Every exception the SDK raises is importable from diagnos, and every one of them names a decision you have to make: fix the input, wait, re-enroll, ask an admin, or report a bug. Catch by class, never by message — messages are for people and may change; classes and codes are the contract. The CLI and the REST API translate the same classes into exit codes and HTTP statuses, so this page is the one table for all three.

The hierarchy#

classDiagram
    DiagnosError <|-- ConfigError
    DiagnosError <|-- EnrollmentDeniedError
    DiagnosError <|-- EnrollmentExpiredError
    DiagnosError <|-- SessionExpiredError
    DiagnosError <|-- GroupKeyUnavailable
    DiagnosError <|-- CryptoError
    DiagnosError <|-- ProtocolError
    DiagnosError <|-- VaultError
    VaultError <|-- ValidationError
    VaultError <|-- AuthenticationError
    VaultError <|-- QuotaError
    VaultError <|-- DiagnosPermissionError
    VaultError <|-- NotFoundError
    VaultError <|-- ConflictError
    VaultError <|-- RateLimitError

VaultError and its subclasses mean the vault answered with an error; they carry the vault's code, the HTTP status, and the trace_id and request_id a support ticket needs. Everything else under DiagnosError is decided locally, before or after talking to the vault.

Every exception#

Class Raised when What to do CLI exit API status
ConfigError DIAGNOS_API_TOKEN missing or malformed; an invalid env var; OpenBao configured without the openbao extra fix the configuration 2 exits 2 at startup
EnrollmentDeniedError an admin denied the enrollment nothing to retry — ask why 3 startup fails
EnrollmentExpiredError nobody approved before expires_at run again for a new link and code 3 startup fails
SessionExpiredError a signed call with no live session unlock() again (resource calls do it for you) 3 401 session_expired
GroupKeyUnavailable the data's security group was not granted to this enrollment a new enrollment that includes it 3 403 group_key_unavailable
CryptoError an envelope did not open: wrong key or tampered bytes, deliberately indistinguishable do not retry; report it 1 500 crypto_error
ProtocolError the vault answered something the protocol does not allow do not retry; report it with the SDK version, or upgrade 1 502 protocol_error
ValidationError the vault refused the request as invalid (400, 413) fix the input 1 400
AuthenticationError token, session or signature rejected (401) usually re-enroll 3 401
QuotaError the workspace has no credit for this (402); nothing was done buy credit, or wait for the budget 5 402
DiagnosPermissionError this service account may not do this here (403), incl. a revoked token ask an admin 3 403
NotFoundError no such document, version or file (404) check the id 4 404
ConflictError a pending or newer version, a replay, or an upload that never reached storage (409) read again and merge, or upload again 7 409
RateLimitError still rate limited after the SDK's own backoff (429) slow down; retry later 6 429
VaultError any other vault error — a 5xx after one retry, or a response that was not JSON (InvalidResponse) retry later; report with trace_id if it persists 1 502

Which vault code maps to which class — DocumentVersionMismatch, QuotaExceeded, ServiceAccountRevoked and the rest — is normative, and lives in PROTOCOL.md §12.

Three failures are not DiagnosErrors, because they are programming errors caught before anything is sent:

Exception When
pydantic.ValidationError a record with a misspelled or invalid field — see Patients
TypeError security_group given as a list — a document belongs to exactly one group
ValueError a naive datetime, an unparseable date, an upload of bytes without a name

MemoryLockWarning is a warning, not an error: the OS refused to lock a key in RAM, and the process runs on with every other protection. DIAGNOS_MEMORY_LOCK=require turns it into a hard failure — see Configuration.

What the SDK already retries#

Before you wrap a call in a retry loop: the SDK already retries everything that is safe to retry, with a fresh signature each time, and raises only when retrying stopped making sense.

Condition What the SDK does Then raises
first signed request of a process syncs the clock with GET /time (median of three) —
401 SignatureTimestampSkew resyncs the clock, retries once AuthenticationError
409 ReplayDetected retries once with a new nonce ConflictError
429 waits Retry-After if sent, else 0.5 s, 1 s, 2 s with jitter — three retries RateLimitError
any 5xx one retry after 1 s VaultError
409 DocumentVersionPending on a write another writer holds the slot: retries after 1.5 s and 3 s ConflictError
a commit lost on the network or to a 5xx replays it after 0.5 s and 1 s — commits are idempotent VaultError
a multipart upload failing midway aborts it, so the vault releases the space the original error

What is left for you: ConflictError with DocumentVersionMismatch means someone saved in between — read again, merge, write again (example). ConflictError with UploadIncomplete means a file's bytes never reached storage — upload that file again. RateLimitError and VaultError are worth one more try after a longer pause.

Clock skew#

Every signed request carries a timestamp the vault accepts only within ±120 seconds of its own clock. A laptop with a wrong clock or a container without NTP would sign every request into a rejection, so the SDK measures the offset against the vault before its first signed call and applies it to every signature, and resyncs once if the vault still complains. You do not configure any of this.

Note

The offset corrects signatures only. Session expiry is judged by the local clock, so a clock that is hours off makes sessions look expired too early or too late. Keep NTP running on anything that holds a session.

Reading an error#

from diagnos import Diagnos, NotFoundError, VaultError

vault = Diagnos()
try:
    vault.patients.get("pat_does_not_exist")
except NotFoundError as error:
    print(error.code, error.status)  # DocumentNotFound 404
except VaultError as error:
    print("vault error", error.code, error.status, error.trace_id, error.request_id)

Log code, status, trace_id and request_id — never the record you were writing. trace_id is what diagnos support needs to find the event on the vault's side.

In the CLI and the REST API#

The CLI prints a one-line, bilingual label and the message on stderr, and exits with the code in the table above — 0 on success. Branch on the exit code in scripts; never grep the text:

diagnos --quiet patients get "$PATIENT_ID" > patient.txt
case $? in
  0) echo "ok" ;;
  3) echo "re-enroll, or ask an admin for access" ;;
  4) echo "no such patient" ;;
  *) echo "failed" ;;
esac

The REST API answers every non-2xx with one envelope, whose code is the vault's own when the failure came from the vault:

{ "error": { "code": "DocumentNotFound", "message": "…", "trace_id": "…" } }

Besides the statuses in the table, it answers 422 (invalid_request) for a body or query that fails validation, 401 (client_certificate_required) and 403 (client_certificate_cn_not_allowed) for mutual-TLS failures, and 404/405 for an unknown route or method — always in the same envelope. The API reference documents them per route.