ytmapi_rs/
error.rs

1//! Module to contain code related to errors that could be produced by the API.
2use core::fmt::{Debug, Display};
3pub use json_crawler::CrawlerError as JsonError;
4use std::{
5    hash::{Hash, Hasher},
6    io,
7    time::SystemTimeError,
8};
9
10/// Alias for a Result with the error type ytmapi-rs::Error.
11pub type Result<T> = core::result::Result<T, Error>;
12
13/// This type represents all errors this API could produce.
14pub struct Error {
15    // This is boxed to avoid passing around very large errors - in the case of an Api error we
16    // want to provide the source file to the caller.
17    inner: Box<ErrorKind>,
18}
19
20/// The kind of the error.
21/// This list may grow over time, and it's not recommended to exhaustively match
22/// on it.
23#[non_exhaustive]
24pub enum ErrorKind {
25    /// Error parsing Json response from InnerTube.
26    JsonParsing(JsonError),
27    /// Error from HTTP client.
28    Web { message: String },
29    /// General io error.
30    // TODO: improve
31    Io(io::Error),
32    /// Received a response from InnerTube that was not in the expected (JSON)
33    /// format.
34    InvalidResponse { response: String },
35    /// InnerTube credential header not in expected format.
36    Header,
37    UnableToSerializeGoogleOAuthToken {
38        response: String,
39        err: serde_json::Error,
40    },
41    /// InnerTube rejected the User Agent we are using.
42    InvalidUserAgent(String),
43    /// Failed to authenticate using Browse Auth credentials (may have expired,
44    /// or been incorrectly provided).
45    BrowserAuthenticationFailed,
46    /// OAuthToken has expired.
47    /// Returns a hash of the expired token generated using the default hasher.
48    OAuthTokenExpired { token_hash: u64 },
49    // This is a u64 not a usize as that is what serde_json will deserialize to.
50    // TODO: Could use a library to handle these.
51    /// Recieved an error code in the Json reply from InnerTube.
52    OtherErrorCodeInResponse { code: u64, message: String },
53    /// Innertube returned a STATUS_FAILED for the query.
54    ApiStatusFailed,
55    /// Unable to obtain system time for the query to Innertube.
56    SystemTimeError { message: String },
57}
58/// The type we were attempting to pass from the Json.
59#[derive(Debug, Clone)]
60pub enum ParseTarget {
61    Array,
62    Other(String),
63}
64impl Error {
65    /// Extract the inner kind from the error for pattern matching.
66    pub fn into_kind(self) -> ErrorKind {
67        *self.inner
68    }
69    pub(crate) fn invalid_user_agent<S: Into<String>>(user_agent: S) -> Self {
70        Self {
71            inner: Box::new(ErrorKind::InvalidUserAgent(user_agent.into())),
72        }
73    }
74    pub(crate) fn oauth_token_expired(token: &crate::auth::OAuthToken) -> Self {
75        let mut h = std::hash::DefaultHasher::new();
76        token.hash(&mut h);
77        let token_hash = h.finish();
78        Self {
79            inner: Box::new(ErrorKind::OAuthTokenExpired { token_hash }),
80        }
81    }
82    pub(crate) fn browser_authentication_failed() -> Self {
83        Self {
84            inner: Box::new(ErrorKind::BrowserAuthenticationFailed),
85        }
86    }
87    pub(crate) fn header() -> Self {
88        Self {
89            inner: Box::new(ErrorKind::Header),
90        }
91    }
92    pub(crate) fn response<S: Into<String>>(response: S) -> Self {
93        let response = response.into();
94        Self {
95            inner: Box::new(ErrorKind::InvalidResponse { response }),
96        }
97    }
98    pub(crate) fn unable_to_serialize_oauth<S: Into<String>>(
99        response: S,
100        err: serde_json::Error,
101    ) -> Self {
102        let response = response.into();
103        Self {
104            inner: Box::new(ErrorKind::UnableToSerializeGoogleOAuthToken { response, err }),
105        }
106    }
107    pub(crate) fn other_code(code: u64, message: String) -> Self {
108        Self {
109            inner: Box::new(ErrorKind::OtherErrorCodeInResponse { code, message }),
110        }
111    }
112    pub(crate) fn status_failed() -> Self {
113        Self {
114            inner: Box::new(ErrorKind::ApiStatusFailed),
115        }
116    }
117    pub(crate) fn web(message: impl Into<String>) -> Self {
118        Self {
119            inner: Box::new(ErrorKind::Web {
120                message: message.into(),
121            }),
122        }
123    }
124}
125
126impl std::error::Error for Error {}
127impl Display for ErrorKind {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        match self {
130            ErrorKind::Web { message } => write!(f, "Web error <{message}> received."),
131            ErrorKind::Io(e) => write!(f, "IO error {e} recieved."),
132            ErrorKind::Header => write!(f, "Error parsing header."),
133            ErrorKind::InvalidResponse { response } => {
134                write!(
135                    f,
136                    "Response is invalid json - unable to deserialize. <{response}>"
137                )
138            }
139            ErrorKind::OtherErrorCodeInResponse { code, message } => {
140                write!(
141                    f,
142                    "Http error code {code} recieved in response. Message: <{message}>."
143                )
144            }
145            ErrorKind::ApiStatusFailed => write!(f, "Api returned STATUS_FAILED for the query"),
146            ErrorKind::OAuthTokenExpired { token_hash: _ } => write!(f, "OAuth token has expired"),
147            ErrorKind::InvalidUserAgent(u) => write!(f, "InnerTube rejected User Agent {u}"),
148            ErrorKind::BrowserAuthenticationFailed => write!(f, "Browser authentication failed"),
149            ErrorKind::UnableToSerializeGoogleOAuthToken { response, err } => write!(
150                f,
151                "Unable to serialize Google auth token {}, received error {}",
152                response, err
153            ),
154            ErrorKind::SystemTimeError { message } => write!(
155                f,
156                "Error obtaining system time to use in API query. <{message}>"
157            ),
158            ErrorKind::JsonParsing(e) => write!(f, "{e}"),
159        }
160    }
161}
162// As this is displayed when unwrapping, we don't want to end up including the
163// entire format of this struct (potentially including entire source json file).
164impl Debug for Error {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        // TODO: Improve implementation
167        Display::fmt(&*self.inner, f)
168    }
169}
170impl Display for Error {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        Display::fmt(&*self.inner, f)
173    }
174}
175impl From<reqwest::Error> for Error {
176    fn from(err: reqwest::Error) -> Self {
177        let message = err.to_string();
178        Self {
179            inner: Box::new(ErrorKind::Web { message }),
180        }
181    }
182}
183impl From<io::Error> for Error {
184    fn from(err: io::Error) -> Self {
185        Self {
186            inner: Box::new(ErrorKind::Io(err)),
187        }
188    }
189}
190impl From<SystemTimeError> for Error {
191    fn from(err: SystemTimeError) -> Self {
192        let message = err.to_string();
193        Self {
194            inner: Box::new(ErrorKind::SystemTimeError { message }),
195        }
196    }
197}
198impl From<ErrorKind> for Error {
199    fn from(value: ErrorKind) -> Self {
200        Self {
201            inner: Box::new(value),
202        }
203    }
204}
205impl From<JsonError> for Error {
206    fn from(value: JsonError) -> Self {
207        let e = ErrorKind::JsonParsing(value);
208        Self { inner: Box::new(e) }
209    }
210}