Skip to main content

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}