Skip to main content

crunchyroll_rs/
error.rs

1//! Library specific errors.
2
3use 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/// Errors that may occur.
13#[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/// Specific error types.
177#[derive(Debug)]
178pub enum ErrorKind {
179    /// Error was caused by something library internal. This only happens if something was
180    /// implemented incorrectly (which hopefully should never be the case) or if Crunchyroll
181    /// surprisingly changed specific parts of their api which broke a part of this crate.
182    Internal,
183    /// Some sort of error occurred while requesting the Crunchyroll api.
184    Request { status: Option<StatusCode> },
185    /// While decoding the api response body something went wrong.
186    Decode { content: Option<Vec<u8>> },
187    /// Something went wrong while logging in.
188    Authentication,
189    /// Malformed or invalid user input.
190    Input,
191    /// When a request got blocked. Currently, this only triggers when the cloudflare bot
192    /// protection is detected.
193    Block {
194        /// HTML/text body of the block response.
195        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    // to ensure compatibility with `T`, convert a empty response to {}
342    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}