mnem_transport/error.rs
1//! Transport-layer error type.
2//!
3//! Kept deliberately coarse-grained: callers distinguish between "the
4//! CAR bytes were malformed" (`Car`), "the blockstore refused a block"
5//! (`Store`), and "the underlying I/O stream failed" (`Io`). Richer
6//! context rides in the variant strings.
7
8use thiserror::Error;
9
10use mnem_core::error::{CodecError, StoreError};
11use mnem_core::id::Cid;
12
13/// Errors raised by CAR export / import.
14#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum TransportError {
17 /// The CAR bytes were malformed (bad header, truncated block,
18 /// invalid varint, wrong CAR version).
19 #[error("car: {0}")]
20 Car(String),
21
22 /// A block inside an imported CAR did not hash to the CID it
23 /// claimed. The caller's CAR is untrusted input; this is rejected
24 /// instead of silently accepting corrupt blocks.
25 #[error("cid mismatch: claimed {claimed}, computed {computed}")]
26 CidMismatch {
27 /// CID the CAR asserted for the block.
28 claimed: Cid,
29 /// CID the importer computed from the payload bytes.
30 computed: Cid,
31 },
32
33 /// A block's CID used a hash algorithm this build does not know how
34 /// to recompute. Currently only SHA-256 and BLAKE3-256 are verified
35 /// on import; anything else is rejected.
36 #[error("unsupported hash algorithm: {0:#x}")]
37 UnsupportedHash(u64),
38
39 /// The CAR header advertised a root CID that was not present in any
40 /// block actually delivered in the body. Rejected so a downstream
41 /// caller can't be deceived into walking a non-existent root.
42 #[error("declared root not present in body: {root}")]
43 MissingRoot {
44 /// Root CID that appeared in the header but never arrived.
45 root: Cid,
46 },
47
48 /// Total block-payload bytes exceeded the import-time cap. Rejected
49 /// mid-stream before the excess data reaches the blockstore.
50 #[error("import exceeded size limit: {observed} > {limit} bytes")]
51 SizeLimit {
52 /// Cap requested by the caller (bytes).
53 limit: u64,
54 /// Observed total at the moment of rejection (bytes).
55 observed: u64,
56 },
57
58 /// Wrapped [`CodecError`] from the underlying DAG-CBOR codec
59 /// (header decode, link extraction).
60 #[error("codec: {0}")]
61 Codec(#[from] CodecError),
62
63 /// Wrapped [`StoreError`] from the target blockstore.
64 #[error("store: {0}")]
65 Store(#[from] StoreError),
66
67 /// Wrapped [`std::io::Error`] from the underlying byte stream.
68 #[error("io: {0}")]
69 Io(#[from] std::io::Error),
70}
71
72/// Errors raised by the async HTTP `RemoteClient` (only meaningful
73/// with the `client` feature). Kept separate from [`TransportError`]
74/// so the pure-sync CAR surface stays free of async / reqwest types.
75///
76/// Variants split the failure modes a caller cares about:
77///
78/// - `Network` - the HTTP request never reached a response (DNS, TLS,
79/// connect, body-read mid-flight). Retryable at the caller's
80/// discretion.
81/// - `Framing` - the response arrived but the CAR body was malformed.
82/// Not retryable on the same URL without a server fix; wraps the
83/// pure-sync [`TransportError`].
84/// - `CasMismatch` - the server rejected `advance-head` because the
85/// stored `old` CID no longer matches the one the client sent.
86/// Caller must refresh refs and retry.
87/// - `Auth` - 401 / 403 on a push endpoint. The bearer token is
88/// missing, expired, or lacks the required capability.
89/// - `Protocol` - the server responded with an unexpected status
90/// code, missing header, or a `mnem-protocol` version mismatch.
91#[derive(Debug, Error)]
92#[non_exhaustive]
93pub enum ClientError {
94 /// Underlying HTTP request failed before a complete response was
95 /// received (connect, TLS handshake, mid-body socket close, ...).
96 #[error("network: {0}")]
97 Network(String),
98
99 /// CAR body received but failed framing / CID verification.
100 #[error("framing: {0}")]
101 Framing(#[from] TransportError),
102
103 /// `advance-head` rejected: the stored `old` value drifted between
104 /// the client's read and its write. Caller refreshes refs and
105 /// retries.
106 #[error(
107 "cas mismatch on ref {ref_name:?}: expected old={expected}, server reports actual={actual}"
108 )]
109 CasMismatch {
110 /// Ref name the client tried to advance (e.g. `main`).
111 ref_name: String,
112 /// `old` CID the client sent.
113 expected: Cid,
114 /// `old` CID the server actually holds.
115 actual: Cid,
116 },
117
118 /// 401 / 403 on a push endpoint. The bearer token is missing,
119 /// expired, or lacks the required capability.
120 #[error("auth failed: {0}")]
121 Auth(String),
122
123 /// Server responded with an unexpected status code, missing
124 /// header, or `mnem-protocol` version mismatch.
125 #[error("protocol: {0}")]
126 Protocol(String),
127
128 /// JSON serialisation or deserialisation failed. Only raised by
129 /// the `client` feature.
130 #[cfg(feature = "client")]
131 #[error("serde: {0}")]
132 Serde(String),
133}
134
135#[cfg(feature = "client")]
136impl From<reqwest::Error> for ClientError {
137 fn from(e: reqwest::Error) -> Self {
138 // `reqwest::Error` carries enough context in its Display impl
139 // to diagnose connect / TLS / body-read failures.
140 Self::Network(e.to_string())
141 }
142}
143
144#[cfg(feature = "client")]
145impl From<serde_json::Error> for ClientError {
146 fn from(e: serde_json::Error) -> Self {
147 Self::Serde(e.to_string())
148 }
149}