1#![doc = include_str!("../README.md")]
2
3use std::{fmt, io, path::PathBuf, result, time};
4
5use thiserror::Error;
6
7pub type Result<T, E = Error> = result::Result<T, E>;
8
9#[derive(Debug, Error)]
10pub enum Error {
11 #[error(transparent)]
12 IO(#[from] io::Error),
13
14 #[cfg(feature = "bitcoincore-rpc")]
15 #[error(transparent)]
16 BitcoinRPC(#[from] bitcoincore_rpc::Error),
17
18 #[cfg(feature = "corepc")]
19 #[error(transparent)]
20 CorepcRPC(#[from] corepc_client::client_sync::Error),
21
22 #[cfg(feature = "jiff")]
23 #[error(transparent)]
24 Jiff(#[from] jiff::Error),
25
26 #[cfg(feature = "fjall")]
27 #[error(transparent)]
28 Fjall(#[from] fjall::Error),
29
30 #[cfg(feature = "vecdb")]
31 #[error(transparent)]
32 VecDB(#[from] vecdb::Error),
33
34 #[cfg(feature = "vecdb")]
35 #[error(transparent)]
36 RawDB(#[from] vecdb::RawDBError),
37
38 #[cfg(feature = "ureq")]
39 #[error(transparent)]
40 Ureq(#[from] ureq::Error),
41
42 #[error(transparent)]
43 SystemTimeError(#[from] time::SystemTimeError),
44
45 #[cfg(feature = "bitcoin")]
46 #[error(transparent)]
47 BitcoinConsensusEncode(#[from] bitcoin::consensus::encode::Error),
48
49 #[cfg(feature = "bitcoin")]
50 #[error(transparent)]
51 BitcoinBip34Error(#[from] bitcoin::block::Bip34Error),
52
53 #[cfg(feature = "bitcoin")]
54 #[error(transparent)]
55 BitcoinHexError(#[from] bitcoin::consensus::encode::FromHexError),
56
57 #[cfg(feature = "bitcoin")]
58 #[error(transparent)]
59 BitcoinFromScriptError(#[from] bitcoin::address::FromScriptError),
60
61 #[cfg(feature = "bitcoin")]
62 #[error(transparent)]
63 BitcoinHexToArrayError(#[from] bitcoin::hex::HexToArrayError),
64
65 #[cfg(feature = "pco")]
66 #[error(transparent)]
67 Pco(#[from] pco::errors::PcoError),
68
69 #[cfg(feature = "serde_json")]
70 #[error(transparent)]
71 SerdeJSON(#[from] serde_json::Error),
72
73 #[cfg(feature = "tokio")]
74 #[error(transparent)]
75 TokioJoin(#[from] tokio::task::JoinError),
76
77 #[error("ZeroCopy error")]
78 ZeroCopyError,
79
80 #[error("Wrong length, expected: {expected}, received: {received}")]
81 WrongLength { expected: usize, received: usize },
82
83 #[error("Wrong address type")]
84 WrongAddrType,
85
86 #[error("Date cannot be indexed, must be 2009-01-03, 2009-01-09 or greater")]
87 UnindexableDate,
88
89 #[error("Quick cache error")]
90 QuickCacheError,
91
92 #[error("The provided address appears to be invalid")]
93 InvalidAddr,
94
95 #[error("Invalid network")]
96 InvalidNetwork,
97
98 #[error("The provided TXID appears to be invalid")]
99 InvalidTxid,
100
101 #[error("Mempool data is not available")]
102 MempoolNotAvailable,
103
104 #[error("Address not found in the blockchain (no transaction history)")]
105 UnknownAddr,
106
107 #[error("Failed to find the TXID in the blockchain")]
108 UnknownTxid,
109
110 #[error("Unsupported type ({0})")]
111 UnsupportedType(String),
112
113 #[error("{0}")]
115 NotFound(String),
116
117 #[error("{0}")]
118 OutOfRange(String),
119
120 #[error("{0}")]
121 Parse(String),
122
123 #[error("Internal error: {0}")]
124 Internal(&'static str),
125
126 #[error("Authentication failed")]
127 AuthFailed,
128
129 #[error("{0}")]
131 SeriesNotFound(SeriesNotFound),
132
133 #[error("'{series}' doesn't support the requested index. Try: {supported}")]
134 SeriesUnsupportedIndex { series: String, supported: String },
135
136 #[error("No series specified")]
137 NoSeries,
138
139 #[error("No data available")]
140 NoData,
141
142 #[error("Request weight {requested} exceeds maximum {max}")]
143 WeightExceeded { requested: usize, max: usize },
144
145 #[error("Deserialization error: {0}")]
146 Deserialization(String),
147
148 #[error("Fetch failed after retries: {0}")]
149 FetchFailed(String),
150
151 #[error("HTTP {status}: {url}")]
152 HttpStatus { status: u16, url: String },
153
154 #[error("Version mismatch at {path:?}: expected {expected}, found {found}")]
155 VersionMismatch {
156 path: PathBuf,
157 expected: usize,
158 found: usize,
159 },
160}
161
162impl Error {
163 #[cfg(feature = "vecdb")]
166 pub fn is_lock_error(&self) -> bool {
167 matches!(self, Error::VecDB(e) if e.is_lock_error())
168 }
169
170 #[cfg(feature = "vecdb")]
173 pub fn is_data_error(&self) -> bool {
174 matches!(self, Error::VecDB(e) if e.is_data_error())
175 || matches!(self, Error::VersionMismatch { .. })
176 }
177
178 pub fn is_network_permanently_blocked(&self) -> bool {
182 match self {
183 #[cfg(feature = "ureq")]
184 Error::Ureq(e) => is_ureq_error_permanent(e),
185 Error::IO(e) => is_io_error_permanent(e),
186 Error::HttpStatus { status, .. } => *status == 403,
188 _ => false,
190 }
191 }
192}
193
194#[cfg(feature = "ureq")]
195fn is_ureq_error_permanent(e: &ureq::Error) -> bool {
196 let msg = format!("{:?}", e);
197 msg.contains("nodename nor servname")
198 || msg.contains("Name or service not known")
199 || msg.contains("No such host")
200 || msg.contains("connection refused")
201 || msg.contains("Connection refused")
202 || msg.contains("certificate")
203 || msg.contains("SSL")
204 || msg.contains("TLS")
205 || msg.contains("handshake")
206}
207
208fn is_io_error_permanent(e: &std::io::Error) -> bool {
209 use std::io::ErrorKind::*;
210 match e.kind() {
211 ConnectionRefused | PermissionDenied | AddrNotAvailable => true,
213 _ => {
215 let msg = e.to_string();
216 msg.contains("nodename nor servname")
217 || msg.contains("Name or service not known")
218 || msg.contains("No such host")
219 }
220 }
221}
222
223#[derive(Debug)]
224pub struct SeriesNotFound {
225 pub series: String,
226 pub suggestions: Vec<String>,
227 pub total_matches: usize,
228}
229
230impl SeriesNotFound {
231 pub fn new(mut series: String, all_matches: Vec<String>) -> Self {
232 let total_matches = all_matches.len();
233 let suggestions = all_matches.into_iter().take(3).collect();
234 if series.len() > 100 {
235 series.truncate(100);
236 series.push_str("...");
237 }
238 Self {
239 series,
240 suggestions,
241 total_matches,
242 }
243 }
244}
245
246impl fmt::Display for SeriesNotFound {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 write!(f, "'{}' not found", self.series)?;
249
250 if self.suggestions.is_empty() {
251 return Ok(());
252 }
253
254 let quoted: Vec<_> = self.suggestions.iter().map(|s| format!("'{s}'")).collect();
255 write!(f, ", did you mean {}?", quoted.join(", "))?;
256
257 let remaining = self.total_matches.saturating_sub(self.suggestions.len());
258 if remaining > 0 {
259 write!(
260 f,
261 " ({remaining} more — /api/series/search?q={} for all)",
262 self.series
263 )?;
264 }
265
266 Ok(())
267 }
268}