#[non_exhaustive]pub enum AcdpError {
Show 38 variants
Canonicalization(String),
HashMismatch {
stored: ContentHash,
recomputed: ContentHash,
},
ContextIdMismatch {
requested: String,
served: String,
},
IncompleteLineage {
lineage_id: String,
ctx_id: Option<String>,
},
SearchTruncated(String),
RevocationDiscoveryBudgetExceeded(String),
RevocationDiscoveryFailed {
source: Box<AcdpError>,
},
RemoteHashMismatch(String),
DataRefHashMismatch(String),
InvalidSignature(String),
KeyResolution(String),
KeyResolutionUnreachable(String),
KeyNotAuthorized(String),
InvalidBody(String),
MissingField(&'static str),
SchemaViolation(String),
PayloadTooLarge(String),
EmbeddedTooLarge(String),
UnsupportedAlgorithm(String),
UnsupportedMediaType(String),
NotImplemented(String),
NotFound(String),
NotAuthorized(String),
RateLimited(String),
CursorExpired,
InvalidCursor(String),
SupersededTarget {
reason: SupersessionReason,
message: String,
},
DuplicatePublish(String),
CrossRegistryResolutionFailed(String),
InvalidReceipt(String),
InvalidLogProof(String),
InvalidWitnessCosignature(String),
ImmutableField(String),
InvalidLifecycleTransition(String),
RegistryInternal(String),
Registry(WireError),
Serialization(String),
Http(String),
}Expand description
Top-level error type.
#[non_exhaustive]: the wire vocabulary (RFC-ACDP-0007 §5) keeps growing
as new RFCs land — 25 wire codes now, up from 21 not long ago, with
RFC-ACDP-0009 reserved and still unimplemented. Without this attribute,
every new variant is a semver-breaking change for any downstream crate
that matches on AcdpError exhaustively. Same rationale as
SsrfReason in crates/acdp-safe-http/src/lib.rs (“future spec
revisions may add ranges”); match with a wildcard arm.
Clone (added for issue #248 Phase 4): VerifiedContext and
VerificationReport each need an independently-owned copy of a
ProceedWithKnown-swallowed revocation-discovery failure — one via
VerifiedContext::revocation_discovery_failure(), the other via
VerificationReport::revocation_discovery — so both surfaces stay
non-silent from a single call. Every variant field (String,
&'static str, ContentHash, WireError, SupersessionReason,
and the recursive Box<AcdpError> in RevocationDiscoveryFailed)
is already Clone, so this is a free addition with no wire-format
or matching impact.
This is a standing constraint on every future variant, not just the
ones that exist today: every future variant’s payloads must remain
Clone. A variant that needs a structured source should hold
Arc<dyn std::error::Error + Send + Sync> (which is Clone
regardless of the inner type, and still preserves source()
chaining), never Box<dyn Error> or a bare io::Error (neither is
Clone). This is not a new tradeoff introduced by adding Clone
here — From<std::io::Error> and From<reqwest::Error> already
stringify into Http(String) rather than carry the error value, so
value semantics over reference/source-chain semantics was already
the twice-exercised choice for this type; Clone just makes it a
documented rule instead of an implicit pattern.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
Canonicalization(String)
JCS canonicalization failed (input not serializable).
HashMismatch
Stored content_hash did not match the recomputed value
(locally detected during signature verification).
Fields
stored: ContentHashThe hash claimed by the body or request.
recomputed: ContentHashThe hash recomputed by the verifier.
ContextIdMismatch
Locally detected: the registry returned a body whose ctx_id is not
the one that was requested (context substitution). Not a wire code —
the client detects it. Permanent; fail closed.
Implements RFC-ACDP-0006 §4.1 step 7 (NORMATIVE, “Bind the resolved
identity”): step 7 requires exactly this comparison — body.ctx_id
against the ctx_id used to construct the request — and permits a
consumer to surface “an equivalent typed error” in place of the
registry-side cross_registry_resolution_failed wire code; this
variant is that typed error. See RFC-ACDP-0008 §9.1 for the threat
this closes: without it, a registry can serve any other
validly-signed body by the same producer under the requested
context’s URL, and both signature verification and content_hash
recomputation still pass. It does not close §9.1 in full: a
registry that genuinely republishes the same content under a new
ctx_id still passes; only serve-time substitution — a different
id claimed to be the one requested — is caught.
Fields
IncompleteLineage
Locally detected: GET /lineages/{id} (RFC-ACDP-0013 §8.1) did not
serve a lineage the caller could accept — either it came back with
no members at all, or (when ctx_id is Some) its members did not
include the ctx_id a live search match named for that lineage.
Not a wire code — RFC-ACDP-0014 §10 (“No new wire error code”):
this is a locally-detected consumer-side binding failure, exactly
the same kind of thing as AcdpError::ContextIdMismatch (issue
#189) but for the lineage-walk path added for issue #226. Permanent;
fail closed — never added to AcdpError::is_transient.
Fields
ctx_id: Option<String>The ctx_id a live search match named for this lineage, when
the walk was invoked with a specific member to check for.
None when the walk was invoked directly with no particular
member to verify (e.g. via find_revocations_in_lineage),
in which case only the empty-lineage case can produce this
variant.
SearchTruncated(String)
Locally detected: a revocation-discovery search helper
(find_revocations, find_registry_attested_revocations in
acdp-client) exhausted its pagination safety cap
(MAX_SEARCH_PAGES) with a search cursor still remaining, or
named more candidate revocation-lineage ids than its lineage-walk
safety cap (MAX_LINEAGE_WALKS) allows to fetch. Not a wire code
— RFC-ACDP-0014 §10 (“No new wire error code”): this is a
client-local safety-limit signal the registry cannot express, the
same rationale as AcdpError::ContextIdMismatch and
AcdpError::IncompleteLineage. Permanent for the same request
shape — never added to AcdpError::is_transient; retrying an
identical query reproduces the same truncation.
Both caps exist as hostile-registry DoS protection
(MAX_SEARCH_PAGES bounds search round-trips; MAX_LINEAGE_WALKS
bounds the GET /lineages/{id} fetches a search result can
trigger, each capped at 1 MB). Search results are ordered
created_at DESC, so hitting a cap sheds the OLDEST — most
likely earliest-compromised_since — members first: a silently
truncated revocation set narrows a compromise window, which is
precisely the outcome RFC-ACDP-0014 §4 (“earliest T across a
revocation lineage is effective”) forbids. A knowingly-partial
answer from these discovery helpers is strictly worse than a
loud refusal, so exhausting either cap with more results
remaining on the registry is a hard error, never a silently
partial Vec.
RevocationDiscoveryBudgetExceeded(String)
Locally detected (issue #258): RFC-ACDP-0014 §8 revocation
auto-discovery (RevocationDiscovery::max_requests /
RevocationDiscovery::max_bytes in acdp-client) exhausted its
caller-configured request-count or cumulative-byte budget before
both trust-class lookups finished. Not a wire code — RFC-ACDP-0014
§10 (“No new wire error code”) forbids one, and this is
client-side only, the same rationale as
AcdpError::SearchTruncated. Permanent for the same request
shape — never added to AcdpError::is_transient — exhausting a
budget is closer to SearchTruncated (“we did not see
everything”) than to a transport error, and carries the same
attacker-inducible-downgrade warning: a hostile registry that
learns a caller’s budget can pad harmless-looking traffic to
exhaust it before a real revocation is found.
The two budgets are combined across BOTH lookups
(find_revocations and, when opted in,
find_registry_attested_revocations), not one ceiling each —
enabling the registry-attested trust class does not double the
allowance. The request count is enforced exactly: the slot is
reserved before the request is issued, so a request is never
issued once the budget is reserved out, and — unlike the byte
count below — a 503, a parse failure, or a PayloadTooLarge on
an already-reserved request still consumes its slot. The byte
count is enforced on a check-before-issue basis using the running
total from completed requests, so up to TWO in-flight requests
(one per concurrently-running trust-class lookup) can push the
total over max_bytes before the NEXT request observes the
overrun. Counts registry traffic only (acdp-client’s
RegistryClient::{capabilities, retrieve, lineage, search}) —
DID-document fetches issued via WebResolver are not counted.
Only the byte count is scoped to successfully-parsed response
bodies: the capped error-envelope read on a non-success response
is never charged to max_bytes, but IS charged to max_requests
(the slot was already reserved before the response arrived).
RevocationDiscoveryFailed
Locally detected: RFC-ACDP-0014 §8 revocation auto-discovery
(RevocationPolicy::discover in acdp-client) failed under
DiscoveryFailurePolicy::FailClosed. Not a wire code — the same
rationale as AcdpError::ContextIdMismatch,
AcdpError::IncompleteLineage, and AcdpError::SearchTruncated:
this is a client-local decision about how to react to a failure
the registry already reported some other way, so it wraps that
failure rather than inventing a new wire vocabulary entry for it.
source is boxed rather than inline: this variant recurses
(AcdpError containing AcdpError), and an unboxed recursive
field would make the enum infinite-sized. Box<AcdpError> still
gets Clone for free from AcdpError’s own derive (added for
issue #248 Phase 4 — see the enum’s doc), which is what lets a
ProceedWithKnown-swallowed discovery failure be independently
owned by both VerifiedContext::revocation_discovery_failure
and VerificationReport::revocation_discovery from one call.
AcdpError still has no PartialEq.
Fields
source: Box<AcdpError>The underlying error discovery hit — typically a transport
failure from the search or lineage-walk requests
(AcdpError::KeyResolutionUnreachable, AcdpError::Http,
…) or AcdpError::SearchTruncated from exhausting a
discovery safety cap. AcdpError::is_transient delegates
to this field, so a retry-aware caller still gets a correct
answer through the wrapper.
RemoteHashMismatch(String)
Wire code: hash_mismatch. The remote registry rejected a
publish request because its independent hash recomputation did
not match the producer-supplied content_hash. Distinct from
the local AcdpError::HashMismatch variant: this one carries
the registry’s message verbatim and indicates a producer-side
bug (most often canonicalization divergence — see RFC-ACDP-0001
§5.7 and the can-001 conformance fixture).
DataRefHashMismatch(String)
Wire code: data_ref_hash_mismatch. A DataRef’s fetched or decoded
bytes do not match the producer-declared data_ref.content_hash.
The body itself remains cryptographically valid — only the
referenced data has diverged. Distinct from
AcdpError::RemoteHashMismatch / AcdpError::HashMismatch
(body-level ProducerContent failure — the whole body is untrusted)
and AcdpError::InvalidSignature (a key / key-binding problem).
RFC-ACDP-0002 §6.5–6.6, RFC-ACDP-0007 §5.
InvalidSignature(String)
Signature verification failed or signature was malformed.
Wire code: invalid_signature.
KeyResolution(String)
Wire code: key_resolution_failed (HTTP 400).
KeyResolutionUnreachable(String)
Wire code: key_resolution_unreachable (HTTP 502) — transient, may retry.
KeyNotAuthorized(String)
Wire code: key_not_authorized (HTTP 403).
InvalidBody(String)
Producer body could not be parsed.
MissingField(&'static str)
A required field was missing.
SchemaViolation(String)
Schema validation failed (string length, array uniqueness, oneOf, etc).
Wire code: schema_violation.
PayloadTooLarge(String)
Wire code: payload_too_large — request body exceeds the registry limit.
EmbeddedTooLarge(String)
Wire code: embedded_too_large — a single DataRef.embedded.content
exceeds the 64 KB cap.
UnsupportedAlgorithm(String)
Wire code: unsupported_algorithm — the producer used a signature
algorithm the registry does not accept.
UnsupportedMediaType(String)
Wire code: unsupported_media_type — the request carried a body whose
Content-Type is outside the registry’s accept-set (RFC-ACDP-0007 §4.1,
§5; HTTP 415). Added on the 0.5.0 line.
Distinct from AcdpError::SchemaViolation on purpose: the body is
rejected unparsed, so no structural claim about it is made. A registry
answering schema_violation here would be asserting a validation that
never ran — and that code is pinned to HTTP 400. Registries advertising
acdp_version below 0.5.0 MUST NOT emit this code.
NotImplemented(String)
Wire code: not_implemented — endpoint or feature not supported by
this registry.
NotFound(String)
Wire code: not_found.
NotAuthorized(String)
Wire code: not_authorized — the caller is not permitted to access
this resource.
RateLimited(String)
Wire code: rate_limited.
CursorExpired
Wire code: cursor_expired.
InvalidCursor(String)
Wire code: invalid_cursor.
SupersededTarget
Wire code: superseded_target. The supersession target was rejected;
the SupersessionReason disambiguates the cause.
Fields
reason: SupersessionReasonWhy the target was rejected.
DuplicatePublish(String)
Wire code: duplicate_publish — an Idempotency-Key replay produced
a different request body than the original.
CrossRegistryResolutionFailed(String)
Wire code: cross_registry_resolution_failed.
InvalidReceipt(String)
Wire code: invalid_receipt. A registry_receipt failed
verification: bad signature, a cross-check mismatch (ctx_id,
content_hash, key_fingerprint, serving authority), a
malformed shape, or a receipt required by policy but absent.
Permanent — the receipt will not verify on retry.
InvalidLogProof(String)
Wire code: invalid_log_proof (RFC-ACDP-0012 §9, §11 — 0.3.0).
A transparency-log artifact failed verification: an inclusion
proof that does not fold to the checkpoint’s root, a failed
consistency proof between tree sizes, or a checkpoint whose
signature does not verify. Permanent — a bad proof will not
verify on retry (HTTP 502 on the wire: the upstream log is at
fault when a federated resolver emits it).
InvalidWitnessCosignature(String)
Wire code: invalid_witness_cosignature (RFC-ACDP-0015 §8,
§10 — 0.4.0). A transparency-log witness cosignature failed
the §8 verification procedure: closed parse, the witness-key
signature, witness binding (signature.key_id DID ≠
witness_id), checkpoint binding (witnessed_checkpoint ≠ the
checkpoint being evaluated), or the witnessed_at skew check.
Deliberately distinct from AcdpError::InvalidLogProof:
that indicts the log (tree membership, history consistency,
the registry’s checkpoint signature); this indicts a witness’s
attestation — an independent verdict over an independent signer
(RFC-ACDP-0015 §10). Permanent — a bad cosignature will not
verify on retry (HTTP 502 on the wire: the cosignature came from
an upstream party — a registry aggregating on a caller’s behalf
or a resolver validating a witness’s cosignatures). A cosignature
that verifies but is merely stale is consumer freshness policy
(§8.1), never this code.
ImmutableField(String)
Wire code: immutable_field (RFC-ACDP-0013 §6, §10 — 0.3.0;
activated from the v0.1.0 reservation). A lifecycle (or future
mutation) endpoint request attempted to supply or alter
immutable body content. Bodies are immutable; lifecycle
endpoints mutate registry state only. Permanent (HTTP 400).
InvalidLifecycleTransition(String)
Wire code: invalid_lifecycle_transition (RFC-ACDP-0013 §6
step 4, §10 — 0.3.0). The requested lifecycle transition
conflicts with the context’s current retraction state (retract
of an already-retracted context; republish of a never-retracted
one). A state conflict like the 409 arm of superseded_target;
retryable only after the state changes (HTTP 409).
RegistryInternal(String)
Wire code: internal_error.
Registry(WireError)
Catch-all for WireError codes that have no typed variant in this
version of the library. Forward-compatible: registries may emit
reserved codes (unsupported_embedding_model)
that future ACDP versions add.
Serialization(String)
JSON (de)serialization failed.
Http(String)
HTTP transport error.
Implementations§
Source§impl AcdpError
impl AcdpError
Sourcepub fn is_transient(&self) -> bool
pub fn is_transient(&self) -> bool
Whether this error is plausibly transient and worth retrying
with the same request body (and, if applicable, the same
Idempotency-Key).
Returned by AcdpError::is_transient only for variants whose
wire codes the spec marks retryable: key_resolution_unreachable
(RFC-ACDP-0001 §5.11), rate_limited (RFC-ACDP-0008 §4.3),
cross_registry_resolution_failed (RFC-ACDP-0006 §7), and
internal_error (RFC-ACDP-0007 §5). Generic Http transport
errors are conservatively treated as transient since they
usually mean DNS or TCP-level glitches.
All cryptographic, schema, and authorization errors are NOT transient: a malformed body or invalid signature will not magically validate on retry.
AcdpError::RevocationDiscoveryFailed is a wrapper, not a
wire code, so it is exempted from the list above and instead
delegates to its own source — a retry-aware caller unwrapping
the wrapper still gets the right answer.
Sourcepub fn from_wire_error(wire: WireError) -> Self
pub fn from_wire_error(wire: WireError) -> Self
Map a wire-protocol [crate::wire_error::WireError] into a typed
AcdpError.
Codes the library does not yet recognize are returned as
AcdpError::Registry for forward compatibility.
Trait Implementations§
Source§impl Error for AcdpError
impl Error for AcdpError
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()