#[non_exhaustive]pub enum AcdpError {
Show 40 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),
RevocationCandidateUnverifiable(String),
RevocationUnknown {
reason: UnknownReason,
source: Box<AcdpError>,
detail: String,
},
KeyRevoked(Box<RevocationRefusal>),
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 RevocationUnknown)
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).
RevocationCandidateUnverifiable(String)
Locally detected (RFC-ACDP-0014 §8, 0.5.0 “Discovery outcome
classes”): a revocation candidate found during auto-discovery
could be neither verified nor proven invalid. The canonical case is
the mandatory historical arm: the candidate’s signing key is in
the producer’s verificationMethod but no longer in
assertionMethod (the producer rotated after publishing it), and
the candidate carries no registry_receipt — or one that does not
verify — so the RFC-ACDP-0010 §10 historical-key rule cannot place
the signature (rev-005 B).
Not a wire code (RFC-ACDP-0014 §10). Not transient — retrying the
same discovery will see the same candidate — but discovery-
fatal: the spec forbids folding an unverifiable candidate into
“not revoked”, so discovery surfaces it (wrapped in
AcdpError::RevocationUnknown) exactly like a transient
lookup failure, and the overall outcome is unknown.
RevocationUnknown
Locally detected (RFC-ACDP-0014 §8, 0.5.0 “Discovery outcome
classes”): revocation auto-discovery (RevocationPolicy::discover
in acdp-client) could not establish whether the signing key is
revoked — the outcome is unknown, which is never folded into
not-revoked. Returned under DiscoveryFailurePolicy::FailClosed
(the default); under ProceedWithKnown the same value is reported
on the verified context instead.
Not a wire code (RFC-ACDP-0014 §10). AcdpError::is_transient is
true exactly when reason is UnknownReason::Transient — map
that to “unavailable, retry” and every other reason to “failed
precondition”.
Replaces 0.14’s RevocationDiscoveryFailed { source }: match
RevocationUnknown { source, .. } for the same shape.
Fields
reason: UnknownReasonThe unknown reason sub-code.
source: Box<AcdpError>The underlying failure (a transport or lookup error,
AcdpError::SearchTruncated,
AcdpError::RevocationDiscoveryBudgetExceeded,
AcdpError::RevocationCandidateUnverifiable, …). Boxed:
the variant recurses.
KeyRevoked(Box<RevocationRefusal>)
Locally detected (RFC-ACDP-0014 §7, 0.5.0 “Attribution”): a
verified, in-scope revocation makes the context’s signing key not
authorized — §7 step 3 (RevocationEffect::Revoked, final) or
step 4 (RevocationEffect::Unplaceable, curable by a receipt).
The refusal names the revocation that fixes the boundary: its
ctx_id, signer, trust class, controller and T.
Wire code: key_not_authorized — a revocation is a verification
verdict with no wire code of its own (RFC-ACDP-0014 §10), so a
round trip through the wire yields AcdpError::KeyNotAuthorized.
AcdpError::is_key_not_authorized covers both.
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.
An SSRF-policy refusal is never Http: the DNS-time refusal raised
by acdp_safe_http::SafeDnsResolver (see SsrfDnsRefusal) is
classified by From<reqwest::Error> as the permanent
AcdpError::SchemaViolation — the same variant the URL-time
SsrfPolicy::check_url refusal already uses — so a retry loop such
as RegistryClient::publish_with_retry does not retry a policy
decision that will be identical on every attempt (issue #321).
CrossRegistryResolver is the one deliberate exception: RFC-ACDP-0007
§5 lists “DNS resolution refused” under
cross_registry_resolution_failed (fixture fed-007), so that
resolver re-maps the refusal to
AcdpError::CrossRegistryResolutionFailed.
All cryptographic, schema, and authorization errors are NOT transient: a malformed body or invalid signature will not magically validate on retry.
AcdpError::RevocationUnknown is not a wire code; it is
transient exactly when its reason is UnknownReason::Transient.
Sourcepub fn unknown_reason(&self) -> UnknownReason
pub fn unknown_reason(&self) -> UnknownReason
The RFC-ACDP-0014 §8 unknown reason a revocation-discovery
failure carrying this error represents (0.5.0 “Discovery outcome
classes”):
UnknownReason::Truncated—AcdpError::SearchTruncated,AcdpError::RevocationDiscoveryBudgetExceeded,AcdpError::IncompleteLineage(truncation is not completeness);UnknownReason::UnverifiableCandidate—AcdpError::RevocationCandidateUnverifiable;UnknownReason::Transient— anySelf::is_transienterror;UnknownReason::Unanswerable— everything else, notablynot_authorized(403) andnot_implemented(501) from the discovery vantage.
An existing AcdpError::RevocationUnknown keeps its own reason.
Whether this is a key_not_authorized verdict:
AcdpError::KeyNotAuthorized or the attributed
AcdpError::KeyRevoked.
Sourcepub fn revocation_refusal(&self) -> Option<&RevocationRefusal>
pub fn revocation_refusal(&self) -> Option<&RevocationRefusal>
The attributed revocation refusal, when this is
AcdpError::KeyRevoked.
Sourcepub fn is_ssrf_policy_refusal(&self) -> bool
pub fn is_ssrf_policy_refusal(&self) -> bool
Whether this error is a client-local SSRF-policy refusal: an
AcdpError::SchemaViolation whose message starts with
SSRF_POLICY_PREFIX.
Covers the URL-time SsrfPolicy::check_url refusal, the pin-time
SsrfPolicy::pin_resolved_ip refusal, and the DNS-time
SafeDnsResolver refusal as classified by From<reqwest::Error>
(issue #321). Use this rather than matching message text yourself.
WebResolver surfaces its refusals as AcdpError::KeyResolution
and CrossRegistryResolver as
AcdpError::CrossRegistryResolutionFailed; neither is reported
here.
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()
Source§impl From<Error> for AcdpError
Available on crate feature reqwest only.
impl From<Error> for AcdpError
reqwest only.Source§fn from(e: Error) -> Self
fn from(e: Error) -> Self
Classify a reqwest transport failure.
- An SSRF-policy refusal anywhere in the
source()chain (is_ssrf_refusal) →AcdpError::SchemaViolation(permanent; same variant as the URL-timeSsrfPolicy::check_urlrefusal). - A connect / timeout failure →
AcdpError::Httpprefixedconnection failed:(transient). - Anything else →
AcdpError::Http(transient).
In every case the message carries error_chain_message, so the
underlying cause is never dropped. The SSRF message is prefixed
with SSRF_POLICY_PREFIX so AcdpError::is_ssrf_policy_refusal
recognizes it.