Error handling

Every error raised by the SDK derives from VerdifaxError. Catch the base class for a single safety net, or catch specific subclasses to branch on failure mode.

Hierarchy

VerdifaxError
├── ValidationError       client-side input validation failed
├── ConnectionError       transport-level failure (DNS / TCP / TLS / timeout)
├── UnsupportedOperationError  method that can no longer give a true answer (verify(), since 0.6.0)
└── APIError              non-2xx HTTP response
    └── StageError        HTTP 422 with error_stage: a deny, a halt, or a stage failure

Import

from verdifax import (
    VerdifaxError,
    ValidationError,
    ConnectionError,
    UnsupportedOperationError,
    APIError,
    StageError,
)

Note that verdifax.ConnectionError is not the builtin ConnectionError from OSError, it intentionally doesn't inherit from OSError so a generic except OSError doesn't silently swallow Verdifax errors.

When each is raised

ExceptionWhenAction
ValidationErrorbad hex, empty route, wrong type, malformed payloadFix the input, never retry
ConnectionErrorDNS failure, connection refused, timeoutRetry with backoff
StageErrorHTTP 422 with error_stage: a sealed deny or halt (PEPG, CCV, MACC, AIVP_T4, PEPG_ADMISSION) or a stage failure; check .stage and .response_bodyFor a deny or halt, fetch the sealed receipt; for a stage failure, investigate the stage
UnsupportedOperationErrorCalling verify(), which always raises since 0.6.0Use the replacement the message names
APIError5xx, 4xx other than the above (including 401 for a missing or invalid key)Retry on 5xx; investigate on 4xx

Branching example

from verdifax import StageError, ConnectionError, ValidationError, APIError

try:
    receipt = client.attest(...)
except ValidationError as e:
    log.error("bad input", error=str(e))
    raise  # don't retry, caller bug
except StageError as e:
    log.error("stage rejected", stage=e.stage, status=e.status_code, error=e.message)
    # a sealed deny or halt (receipt in e.response_body), or a stage failure
    raise
except ConnectionError as e:
    log.warning("transient, retrying", error=str(e))
    raise  # let your retry layer handle it
except APIError as e:
    if 500 <= e.status_code < 600:
        log.warning("server error, retrying", status=e.status_code)
        raise
    log.error("client error", status=e.status_code, error=e.message)
    raise

Retry only on ConnectionError and APIError with status_code >= 500. Use exponential backoff. Never retry ValidationError or StageError.

from tenacity import retry, retry_if_exception_type, wait_exponential, stop_after_attempt

@retry(
    retry=retry_if_exception_type((ConnectionError,)),
    wait=wait_exponential(multiplier=1, min=1, max=30),
    stop=stop_after_attempt(5),
)
def attest_with_retries(client, **kwargs):
    return client.attest(**kwargs)

Continue