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