Skip to main content

bee/swarm/
errors.rs

1//! Crate-level error type. Mirrors bee-go's `BeeError` /
2//! `BeeArgumentError` / `BeeResponseError` hierarchy as a single Rust
3//! enum so callers can `match` on the variant.
4
5use std::fmt;
6
7/// Result alias used across the crate.
8pub type Result<T, E = Error> = std::result::Result<T, E>;
9
10/// Cap on captured response bodies so a huge error page can't OOM.
11pub const RESPONSE_BODY_CAP: usize = 4096;
12
13/// All errors surfaced from `bee-rs`.
14///
15/// - [`Error::Argument`] — caller passed invalid input. Mirrors
16///   bee-js / bee-go `BeeArgumentError`.
17/// - [`Error::Response`] — Bee returned a non-2xx status. Mirrors
18///   bee-js / bee-go `BeeResponseError`.
19/// - [`Error::LengthMismatch`] — typed-byte constructor rejected a
20///   value whose length didn't match any allowed size.
21/// - [`Error::Hex`] — hex decoding failed.
22/// - [`Error::Crypto`] — secp256k1 / keccak operation failed.
23/// - [`Error::Json`] — JSON decode/encode failed.
24/// - [`Error::Transport`] — transport error from `reqwest`.
25/// - [`Error::Other`] — fallback wrapper for anything else.
26#[derive(thiserror::Error, Debug)]
27pub enum Error {
28    /// Invalid argument from the caller.
29    #[error("{message}")]
30    Argument {
31        /// Human-readable description of what was invalid.
32        message: String,
33    },
34
35    /// Non-2xx HTTP response from Bee.
36    #[error("{method} {url}: {status} {status_text}")]
37    Response {
38        /// HTTP method of the failed request.
39        method: String,
40        /// Full URL of the failed request.
41        url: String,
42        /// Numeric status code.
43        status: u16,
44        /// `<code> <reason>` (e.g. `"422 Unprocessable Entity"`).
45        status_text: String,
46        /// Response body, truncated to [`RESPONSE_BODY_CAP`] bytes.
47        body: Vec<u8>,
48    },
49
50    /// Typed-byte constructor received a value of the wrong length.
51    #[error("invalid {kind} length: got {got}, expected {expected:?}")]
52    LengthMismatch {
53        /// The Rust type that rejected the input (`"Reference"`, `"BatchId"`, ...).
54        kind: &'static str,
55        /// Lengths the type accepts.
56        expected: &'static [usize],
57        /// Length actually provided.
58        got: usize,
59    },
60
61    /// Hex decoding failed.
62    #[error("invalid hex: {0}")]
63    Hex(#[from] hex::FromHexError),
64
65    /// Crypto primitive (secp256k1 / keccak) failed.
66    #[error("crypto: {0}")]
67    Crypto(String),
68
69    /// JSON encode/decode failed.
70    #[error("json: {0}")]
71    Json(#[from] serde_json::Error),
72
73    /// Transport-level error (TCP / TLS / DNS).
74    #[error("transport: {0}")]
75    Transport(#[from] reqwest::Error),
76
77    /// Catch-all for errors that don't fit the specific variants.
78    #[error("{0}")]
79    Other(String),
80}
81
82impl Error {
83    /// Build an [`Error::Argument`].
84    pub fn argument<M: Into<String>>(msg: M) -> Self {
85        Error::Argument {
86            message: msg.into(),
87        }
88    }
89
90    /// Build an [`Error::Crypto`].
91    pub fn crypto<M: fmt::Display>(msg: M) -> Self {
92        Error::Crypto(msg.to_string())
93    }
94
95    /// True if this error is an HTTP response error.
96    pub fn is_response(&self) -> bool {
97        matches!(self, Error::Response { .. })
98    }
99
100    /// Get the HTTP status if this error wraps a non-2xx response.
101    pub fn status(&self) -> Option<u16> {
102        match self {
103            Error::Response { status, .. } => Some(*status),
104            _ => None,
105        }
106    }
107}
108
109/// Build an [`Error::Response`] from a `reqwest::Response`. Reads up to
110/// [`RESPONSE_BODY_CAP`] bytes of the body so a giant error page can't
111/// blow up memory.
112pub async fn response_error_from(resp: reqwest::Response) -> Error {
113    let method = resp.url().as_str().to_owned();
114    // reqwest does not expose the request method on Response. Callers
115    // that need the method should wrap with their own context — this
116    // helper is for the common case where Response is the only thing
117    // available. We store the URL twice (in `method` slot too) when
118    // method is unknown; callers that care use [`Error::response`] below.
119    let url = method.clone();
120    let status = resp.status();
121    let status_text = status.canonical_reason().unwrap_or("").to_string();
122    let body = resp
123        .bytes()
124        .await
125        .map(|b| {
126            let n = b.len().min(RESPONSE_BODY_CAP);
127            b.slice(..n).to_vec()
128        })
129        .unwrap_or_default();
130    Error::Response {
131        method: String::new(),
132        url,
133        status: status.as_u16(),
134        status_text: format!("{} {}", status.as_u16(), status_text),
135        body,
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn length_mismatch_displays() {
145        let e = Error::LengthMismatch {
146            kind: "Reference",
147            expected: &[32, 64],
148            got: 16,
149        };
150        assert_eq!(
151            format!("{e}"),
152            "invalid Reference length: got 16, expected [32, 64]"
153        );
154    }
155
156    #[test]
157    fn argument_helper() {
158        let e = Error::argument("bad input");
159        assert!(matches!(e, Error::Argument { .. }));
160        assert_eq!(format!("{e}"), "bad input");
161    }
162}