Errors & Retries
Everything fallible in the crate returns Result<_, acdp::AcdpError>.
AcdpError is a typed, exhaustive mapping of the RFC-ACDP-0007 §5 wire error
codes plus a handful of local/transport errors. This page explains the variants,
how registry wire errors round-trip into them, and which are safe to retry.
The wire error envelope and the canonical code registry are specified in RFC-ACDP-0007 (Capabilities & Errors).
Wire errors round-trip into typed variants
When a registry returns an error envelope, the client maps it via
AcdpError::from_wire_error. Each canonical code becomes a specific variant;
unknown/future codes pass through as AcdpError::Registry(WireError) so a
newer registry never breaks an older client.
| Wire code | AcdpError variant |
|---|---|
invalid_signature | InvalidSignature |
hash_mismatch | RemoteHashMismatch |
data_ref_hash_mismatch | DataRefHashMismatch |
schema_violation | SchemaViolation |
not_authorized | NotAuthorized |
not_found | NotFound |
rate_limited | RateLimited |
payload_too_large | PayloadTooLarge |
embedded_too_large | EmbeddedTooLarge |
key_resolution_failed | KeyResolution |
key_resolution_unreachable | KeyResolutionUnreachable |
key_not_authorized | KeyNotAuthorized |
unsupported_algorithm | UnsupportedAlgorithm |
not_implemented | NotImplemented |
cursor_expired | CursorExpired |
invalid_cursor | InvalidCursor |
duplicate_publish | DuplicatePublish |
cross_registry_resolution_failed | CrossRegistryResolutionFailed |
internal_error | RegistryInternal |
superseded_target | SupersededTarget { reason, message } |
| (unknown) | Registry(WireError) |
This round-trip is exhaustively pinned by the
all_20_wire_codes_round_triptest insrc/error.rs. Adding a new code is a coordinated three-edit change — see below.
Local vs. remote hash mismatches
Two distinct variants exist deliberately:
HashMismatch { stored, recomputed }— you detected it. Thecontent_hashyou recomputed locally doesn't match the one in the body. This is the tamper-detection path in the verification pipeline.RemoteHashMismatch— the registry rejected your publish withhash_mismatch. The hash you sent didn't match the body you sent.
The same split applies to signatures (local verification failure vs. a
registry's invalid_signature rejection — both map to InvalidSignature).
Supersession failures
A superseded_target wire error carries a details.reason sub-vocabulary,
decoded into SupersessionReason:
SupersessionReason | Meaning |
|---|---|
NotFound | the supersedes target doesn't exist on this registry |
LineageMismatch | the target's lineage_id differs from the new publication's |
VersionMismatch | the new version isn't exactly previous.version + 1 |
AlreadySuperseded | the target was already superseded by another version |
CrossRegistrySupersessionUnsupported | v0.1.0 only allows same-registry supersession |
LineageWalkFailed | an intermediate context in the supersedes chain couldn't be retrieved |
Other | a reason this library version doesn't recognize (forward-compat) |
Most of these are prevented up front by using
supersede_body, which sets version,
supersedes, and expected_lineage_id consistently.
Retryability
AcdpError::is_transient() tells you whether retrying the same request body
(with the same Idempotency-Key, if any) is worthwhile. It returns true only
for the variants the spec marks retryable:
Transient (is_transient() == true) | Why |
|---|---|
KeyResolutionUnreachable | the DID host was temporarily unreachable (RFC-ACDP-0001 §5.11) |
RateLimited | back off and retry (RFC-ACDP-0008 §4.3) |
CrossRegistryResolutionFailed | a foreign hop failed transiently (RFC-ACDP-0006 §7) |
RegistryInternal | the registry hit an internal error (HTTP 5xx) |
Http | a connect/timeout/transport error |
Everything else — InvalidSignature, SchemaViolation, HashMismatch,
KeyResolution (permanent resolution failure, including DNS-rebinding
refusals), NotAuthorized, NotFound, PayloadTooLarge — is permanent.
Retrying won't help; fix the request or the key.
RegistryClient::publish_with_retry(req, idempotency_key, max_attempts) uses
exactly this predicate, with bounded backoff (250 ms → 500 ms → 1 s → 2 s):
# #[cfg(feature = "client")]
# async fn run(client: &acdp::client::RegistryClient, req: &acdp::PublishRequest) -> Result<(), acdp::AcdpError> {
let resp = client.publish_with_retry(req, "publish-2026-06-10-abc", 4).await?;
# let _ = resp; Ok(()) }Handling errors
AcdpError is a plain enum — match on it, or use the convenience predicates:
# fn handle(err: acdp::AcdpError) {
use acdp::AcdpError;
match &err {
AcdpError::InvalidSignature(msg) => eprintln!("untrusted context: {msg}"),
AcdpError::NotFound(_) => { /* 404 — nothing to retry */ }
e if e.is_transient() => { /* back off and retry */ }
other => eprintln!("permanent failure: {other}"),
}
# }From conversions are provided for serde_json::Error (→ Serialization),
std::io::Error (→ Http), and reqwest::Error (→ Http, distinguishing
connect/timeout), so ? works naturally in client code.
Adding a new wire error code
If you contribute a new code (per CONTRIBUTING), it's three coordinated edits:
- A new variant in
src/error.rs::AcdpError, with the RFC citation. - A
matcharm inAcdpError::from_wire_error. - Extend the
all_20_wire_codes_round_triptest (and bump its count).
Also revisit is_transient (is the new code retryable?) and
SupersessionReason (if the code uses a details.reason sub-vocabulary).