rosu_v2/
error.rs

1use hyper::{
2    body::Bytes, header::InvalidHeaderValue, http::Error as HttpError, Error as HyperError,
3    StatusCode,
4};
5use serde::Deserialize;
6use serde_json::Error as SerdeError;
7use std::fmt;
8
9#[cfg(feature = "local_oauth")]
10#[cfg_attr(docsrs, doc(cfg(feature = "local_oauth")))]
11#[derive(Debug, thiserror::Error)]
12pub enum OAuthError {
13    #[error("failed to accept request")]
14    Accept(#[source] tokio::io::Error),
15    #[error("failed to create tcp listener")]
16    Listener(#[source] tokio::io::Error),
17    #[error("missing code in request")]
18    NoCode { data: Vec<u8> },
19    #[error("failed to read data")]
20    Read(#[source] tokio::io::Error),
21    #[error("redirect uri must contain localhost and a port number")]
22    Url,
23    #[error("failed to write data")]
24    Write(#[source] tokio::io::Error),
25}
26
27/// The API response was of the form `{ "error": ... }`
28#[derive(Debug, Deserialize, thiserror::Error)]
29pub struct ApiError {
30    /// Error specified by the API
31    pub error: Option<String>,
32}
33
34impl fmt::Display for ApiError {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self.error {
37            Some(ref msg) => f.write_str(msg),
38            None => f.write_str("empty error message"),
39        }
40    }
41}
42
43/// The main error type
44#[derive(Debug, thiserror::Error)]
45#[non_exhaustive]
46pub enum OsuError {
47    /// Failed to create a request body
48    #[error("failed to create request body")]
49    BodyError {
50        #[from]
51        source: HttpError,
52    },
53    /// Failed to build an [`Osu`](crate::Osu) client because no client id was provided
54    #[error("failed to build osu client, no client id was provided")]
55    BuilderMissingId,
56    /// Failed to build an [`Osu`](crate::Osu) client because no client secret was provided
57    #[error("failed to build osu client, no client secret was provided")]
58    BuilderMissingSecret,
59    /// Error while handling response from the API
60    #[error("failed to chunk the response")]
61    ChunkingResponse {
62        #[source]
63        source: HyperError,
64    },
65    /// No usable cipher suites in crypto provider
66    #[error("no usable cipher suites in crypto provider")]
67    ConnectorRoots {
68        #[source]
69        source: std::io::Error,
70    },
71    /// Failed to create the token header for a request
72    #[error("failed to parse token for authorization header")]
73    CreatingTokenHeader {
74        #[from]
75        source: InvalidHeaderValue,
76    },
77    /// The API returned a 404
78    #[error("the osu!api returned a 404 implying a missing score, incorrect name, id, etc")]
79    NotFound,
80    /// Attempted to make request without valid token
81    #[error(
82        "The previous osu!api token expired and the client \
83        has not yet succeeded in acquiring a new one. \
84        Can not send requests until a new token has been acquired. \
85        This should only occur during an extended downtime of the osu!api."
86    )]
87    NoToken,
88    #[cfg(feature = "local_oauth")]
89    #[cfg_attr(docsrs, doc(cfg(feature = "local_oauth")))]
90    /// Failed to perform OAuth
91    #[error("failed to perform oauth")]
92    OAuth {
93        #[from]
94        source: OAuthError,
95    },
96    #[cfg(feature = "replay")]
97    #[cfg_attr(docsrs, doc(cfg(feature = "replay")))]
98    /// There was an error while trying to use osu-db
99    #[error("osu-db error")]
100    OsuDbError {
101        #[from]
102        source: osu_db::Error,
103    },
104    /// Failed to deserialize response
105    #[error("failed to deserialize response: {:?}", .bytes)]
106    Parsing {
107        bytes: Bytes,
108        #[source]
109        source: SerdeError,
110    },
111    /// Failed to parse a value
112    #[error("failed to parse value")]
113    ParsingValue {
114        #[from]
115        source: ParsingError,
116    },
117    /// Failed to send request
118    #[error("failed to send request")]
119    Request {
120        #[source]
121        source: hyper_util::client::legacy::Error,
122    },
123    /// Timeout while requesting from API
124    #[error("osu!api did not respond in time")]
125    RequestTimeout,
126    /// API returned an error
127    #[error("response error, status {}", .status)]
128    Response {
129        bytes: Bytes,
130        #[source]
131        source: ApiError,
132        status: StatusCode,
133    },
134    /// Temporal (?) downtime of the osu API
135    #[error("osu!api may be temporarily unavailable (received 503)")]
136    ServiceUnavailable { body: hyper::body::Incoming },
137    /// The client's authentication is not sufficient for the endpoint
138    #[error("the endpoint is not available for the client's authorization level")]
139    UnavailableEndpoint,
140    /// Failed to update token
141    #[error("failed to update osu!api token")]
142    UpdateToken {
143        #[source]
144        source: Box<OsuError>,
145    },
146    /// Failed to parse the URL for a request
147    #[error("failed to parse URL of a request; url: `{}`", .url)]
148    Url {
149        #[source]
150        source: url::ParseError,
151        /// URL that was attempted to be parsed
152        url: String,
153    },
154}
155
156impl OsuError {
157    pub(crate) fn invalid_mods<E: serde::de::Error>(
158        mods: &serde_json::value::RawValue,
159        err: &SerdeError,
160    ) -> E {
161        E::custom(format!("invalid mods `{mods}`: {err}"))
162    }
163}
164
165/// Failed some [`TryFrom`] parsing
166#[derive(Debug, thiserror::Error)]
167pub enum ParsingError {
168    /// Failed to parse a str into an [`Acronym`](crate::model::mods::Acronym)
169    #[error("failed to parse `{}` into an Acronym", .0)]
170    Acronym(Box<str>),
171    /// Failed to parse a u8 into a [`Genre`](crate::model::beatmap::Genre)
172    #[error("failed to parse {} into Genre", .0)]
173    Genre(u8),
174    /// Failed to parse a String into a [`Grade`](crate::model::Grade)
175    #[error("failed to parse `{}` into Grade", .0)]
176    Grade(String), // TODO: make Box<str>
177    /// Failed to parse a u8 into a [`Language`](crate::model::beatmap::Language)
178    #[error("failed to parse {} into Language", .0)]
179    Language(u8),
180    /// Failed to parse a u8 into a [`MatchTeam`](crate::model::matches::MatchTeam)
181    #[error("failed to parse {} into MatchTeam", .0)]
182    MatchTeam(u8),
183    /// Failed to parse an i8 into a [`RankStatus`](crate::model::beatmap::RankStatus)
184    #[error("failed to parse {} into RankStatus", .0)]
185    RankStatus(i8),
186    /// Failed to parse a u8 into a [`ScoringType`](crate::model::matches::ScoringType)
187    #[error("failed to parse {} into ScoringType", .0)]
188    ScoringType(u8),
189    /// Failed to parse a u8 into a [`TeamType`](crate::model::matches::TeamType)
190    #[error("failed to parse {} into TeamType", .0)]
191    TeamType(u8),
192}