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
| Exception | When | Action |
|---|---|---|
ValidationError | bad hex, empty route, wrong type, malformed payload | Fix the input, never retry |
ConnectionError | DNS failure, connection refused, timeout | Retry with backoff |
StageError | HTTP 422 with error_stage: a sealed deny or halt (PEPG, CCV, MACC, AIVP_T4, PEPG_ADMISSION) or a stage failure; check .stage and .response_body | For a deny or halt, fetch the sealed receipt; for a stage failure, investigate the stage |
UnsupportedOperationError | Calling verify(), which always raises since 0.6.0 | Use the replacement the message names |
APIError | 5xx, 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
Recommended retry policy
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)
