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}