1use reqwest::{Response, StatusCode, Url};
4use serde::Deserialize;
5use serde::de::DeserializeOwned;
6use serde_json::{Map, Value};
7use std::error::Error as StdError;
8use std::fmt::{Debug, Display, Formatter};
9
10pub(crate) type Result<T, E = Error> = core::result::Result<T, E>;
11
12#[derive(Debug)]
14pub struct Error {
15 kind: ErrorKind,
16 source: Option<Box<dyn StdError + Send + Sync>>,
17 url: Option<String>,
18
19 message: Option<String>,
20}
21
22impl Display for Error {
23 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24 let msg_fn = |default: &'static str| {
25 self.message.as_ref().cloned().unwrap_or_else(|| {
26 self.source
27 .as_ref()
28 .map(|s| s.to_string())
29 .unwrap_or_else(|| default.to_string())
30 })
31 };
32
33 let (message, suffix) = match &self.kind {
34 ErrorKind::Internal => (msg_fn("internal error"), None),
35 ErrorKind::Request { status } => {
36 let message = msg_fn("request error");
37 if let Some(status) = &status {
38 (message, Some(format!(" (http {})", status.as_u16())))
39 } else {
40 (message, None)
41 }
42 }
43 ErrorKind::Decode { content } => {
44 let message = msg_fn("decoding error");
45 if let Some(content) = content {
46 (
47 message,
48 Some(format!(": {}", String::from_utf8_lossy(content.as_slice()))),
49 )
50 } else {
51 (message, None)
52 }
53 }
54 ErrorKind::Authentication => (msg_fn("authentication error"), None),
55 ErrorKind::Input => (msg_fn("input error"), None),
56 ErrorKind::Block { body } => {
57 let message = msg_fn("cloudflare blocked");
58 (message, Some(format!(": {}", body)))
59 }
60 };
61
62 if let Some(url) = &self.url {
63 write!(f, "{} at {}{}", message, url, suffix.unwrap_or_default())
64 } else {
65 write!(f, "{}{}", message, suffix.unwrap_or_default())
66 }
67 }
68}
69
70impl StdError for Error {
71 fn source(&self) -> Option<&(dyn StdError + 'static)> {
72 self.source.as_ref().map(|e| &**e as _)
73 }
74}
75
76impl From<serde_json::Error> for Error {
77 fn from(err: serde_json::Error) -> Self {
78 Self {
79 kind: ErrorKind::Decode { content: None },
80 source: Some(err.into()),
81 url: None,
82 message: None,
83 }
84 }
85}
86
87impl From<reqwest::Error> for Error {
88 fn from(err: reqwest::Error) -> Self {
89 let (kind, message) = if err.is_request()
90 || err.is_redirect()
91 || err.is_timeout()
92 || err.is_connect()
93 || err.is_body()
94 || err.is_status()
95 {
96 (
97 ErrorKind::Request {
98 status: err.status(),
99 },
100 None,
101 )
102 } else if err.is_decode() {
103 (ErrorKind::Decode { content: None }, None)
104 } else if err.is_builder() {
105 (ErrorKind::Internal, None)
106 } else {
107 (
108 ErrorKind::Internal,
109 Some("Could not determine request error type - {err}".to_string()),
110 )
111 };
112
113 let url = err.url().map(Url::to_string);
114 Self {
115 kind,
116 source: Some(err.into()),
117 url,
118 message,
119 }
120 }
121}
122
123impl Error {
124 pub fn kind(&self) -> &ErrorKind {
125 &self.kind
126 }
127
128 pub fn message(&self) -> Option<&str> {
129 self.message.as_deref()
130 }
131
132 pub(crate) fn update_msg(mut self, update_fn: fn(Option<String>) -> Option<String>) -> Self {
133 self.message = update_fn(self.message);
134 self
135 }
136
137 pub(crate) fn error_from_kind<S: AsRef<str>>(kind: ErrorKind, msg: S) -> Self {
138 Self {
139 kind,
140 source: None,
141 url: None,
142 message: Some(msg.as_ref().to_string()),
143 }
144 }
145
146 pub(crate) fn error_from_other_error_and_url<
147 E: Into<Box<dyn StdError + Send + Sync>>,
148 U: AsRef<str>,
149 >(
150 other_error: E,
151 kind: ErrorKind,
152 url: U,
153 ) -> Self {
154 Self {
155 kind,
156 source: Some(other_error.into()),
157 url: Some(url.as_ref().to_string()),
158 message: None,
159 }
160 }
161
162 pub(crate) fn error_from_kind_and_url<S: AsRef<str>, U: AsRef<str>>(
163 kind: ErrorKind,
164 url: U,
165 msg: S,
166 ) -> Self {
167 Self {
168 kind,
169 source: None,
170 url: Some(url.as_ref().to_string()),
171 message: Some(msg.as_ref().to_string()),
172 }
173 }
174}
175
176#[derive(Debug)]
178pub enum ErrorKind {
179 Internal,
183 Request { status: Option<StatusCode> },
185 Decode { content: Option<Vec<u8>> },
187 Authentication,
189 Input,
191 Block {
194 body: String,
196 },
197}
198
199pub(crate) fn is_request_error<U: AsRef<str>>(
200 value: Value,
201 url: U,
202 status: &StatusCode,
203) -> Result<()> {
204 #[derive(Debug, Deserialize)]
205 #[serde(untagged)]
206 #[allow(clippy::enum_variant_names)]
207 enum ErrorType {
208 MessageTypeError {
209 message: String,
210 r#type: String,
211 },
212 CodeError {
213 code: String,
214 context: Vec<CodeErrorContext>,
215 #[serde(alias = "error")]
216 message: Option<String>,
217 },
218 GenericError {
219 error: Value,
220 #[serde(flatten)]
221 other: Map<String, Value>,
222 },
223 }
224
225 #[derive(Debug, Deserialize)]
226 struct CodeErrorContext {
227 code: String,
228 #[serde(flatten)]
229 other: Map<String, Value>,
230 }
231
232 let Ok(error_type) = serde_json::from_value::<ErrorType>(value) else {
233 return Ok(());
234 };
235 let error_msg = match error_type {
236 ErrorType::MessageTypeError { message, r#type } => {
237 format!("{type} - {message}")
238 }
239 ErrorType::CodeError {
240 code,
241 context,
242 message,
243 } => {
244 let mut msg = if let Some(message) = message {
245 format!("{message} - {code}")
246 } else {
247 code
248 };
249 if !context.is_empty() {
250 let details: Vec<String> = context
251 .into_iter()
252 .map(|c| format!("{}: {}", c.code, serde_json::to_string(&c.other).unwrap()))
253 .collect();
254 msg += &format!(": ({})", details.join(", "))
255 }
256 msg
257 }
258 ErrorType::GenericError { error, other } => {
259 let mut msg = match error {
260 Value::Number(num) => format!("error {num}"),
261 _ => error.to_string(),
262 };
263 if !other.is_empty() {
264 msg += &format!(": {}", serde_json::to_string(&other).unwrap())
265 }
266 msg
267 }
268 };
269
270 Err(Error {
271 kind: ErrorKind::Request {
272 status: Some(*status),
273 },
274 source: None,
275 url: Some(url.as_ref().to_string()),
276 message: Some(error_msg),
277 })
278}
279
280pub(crate) async fn check_request<T: DeserializeOwned>(resp: Response) -> Result<T> {
281 let url = resp.url().clone();
282 let content_length = resp.content_length().unwrap_or(0);
283 let status = resp.status();
284 let _raw = match resp.status().as_u16() {
285 403 => {
286 let raw = resp.bytes().await?;
287 if raw.starts_with(b"<!DOCTYPE html>")
288 && raw
289 .windows(31)
290 .any(|w| w == b"<title>Just a moment...</title>")
291 {
292 return Err(Error {
293 kind: ErrorKind::Block {
294 body: String::from_utf8_lossy(raw.as_ref()).to_string(),
295 },
296 source: None,
297 url: Some(url.to_string()),
298 message: Some("Triggered Cloudflare bot protection".to_string()),
299 });
300 }
301 raw
302 }
303 404 => {
304 return Err(Error {
305 kind: ErrorKind::Request {
306 status: Some(resp.status()),
307 },
308 source: None,
309 url: Some(url.to_string()),
310 message: Some("The requested resource is not present".to_string()),
311 });
312 }
313 429 => {
314 let retry_secs =
315 if let Some(retry_after) = resp.headers().get(reqwest::header::RETRY_AFTER) {
316 retry_after.to_str().map_or(None, |retry_after_secs| {
317 retry_after_secs.parse::<u32>().ok()
318 })
319 } else {
320 None
321 };
322
323 return Err(Error {
324 kind: ErrorKind::Request {
325 status: Some(resp.status()),
326 },
327 source: None,
328 url: Some(url.to_string()),
329 message: Some(format!(
330 "Rate limit detected. {}",
331 retry_secs.map_or("Try again later".to_string(), |secs| format!(
332 "Try again in {secs} seconds"
333 ))
334 )),
335 });
336 }
337 _ => resp.bytes().await?,
338 };
339 let mut raw: &[u8] = _raw.as_ref();
340
341 if raw.is_empty() && (content_length == 0) {
343 raw = "{}".as_bytes();
344 }
345
346 let value: Value = serde_json::from_slice(raw).map_err(|e| Error {
347 kind: ErrorKind::Decode {
348 content: Some(raw.to_vec()),
349 },
350 source: Some(e.into()),
351 url: Some(url.to_string()),
352 message: None,
353 })?;
354 is_request_error(value.clone(), &url, &status)?;
355 serde_json::from_value::<T>(value).map_err(|e| Error {
356 kind: ErrorKind::Decode {
357 content: Some(raw.to_vec()),
358 },
359 source: Some(e.into()),
360 url: Some(url.to_string()),
361 message: None,
362 })
363}