Lines
76.19 %
Functions
70 %
Branches
100 %
/*
Copyright (C) 2021-2023 Kunal Mehta <legoktm@debian.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use serde::Deserialize;
use serde_json::Value;
use std::fmt::{Display, Formatter};
use std::io;
use thiserror::Error as ThisError;
/// Represents a raw MediaWiki API error, with a error code and error message
/// (text). This is also used for warnings since they use the same format.
#[derive(Clone, Debug, Deserialize)]
pub struct ApiError {
/// Error code
pub code: String,
/// Error message
pub text: String,
/// Extra data
pub data: Option<Value>,
}
impl Display for ApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "(code: {}): {}", self.code, self.text)
/// Possible errors
#[non_exhaustive]
#[derive(ThisError, Debug)]
pub enum Error {
/* Request related errors */
/// A HTTP error like a 4XX or 5XX status code
#[error("HTTP error: {0}")]
HttpError(#[from] reqwest::Error),
/// Invalid header value, likely if the provided OAuth2 token
/// or User-agent are invalid
#[error("Invalid header value: {0}")]
InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
/// Error when decoding the JSON response from the API
#[error("JSON error: {0}")]
InvalidJson(#[from] serde_json::Error),
/// Error if unable to get request concurrency lock
#[error("Unable to get request lock: {0}")]
LockFailure(#[from] tokio::sync::AcquireError),
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
/// A HTTP error with 429 status code
#[error("HTTP 429 Too Many Requests")]
TooManyRequests { retry_after: Option<u64> },
/* Token issues */
/// Token invalid or expired
#[error("Invalid CSRF token")]
BadToken,
/// Unable to fetch a CSRF token
#[error("Unable to get token `{0}`")]
TokenFailure(String),
/* User-related issues */
/// When expected to be logged in but aren't
#[error("You're not logged in")]
NotLoggedIn,
#[error("You're not logged in as a bot account")]
NotLoggedInAsBot,
/* File-related issues */
#[error("Upload warnings: {0:?}")]
UploadWarning(Vec<String>),
/* MediaWiki-side issues */
#[error("maxlag tripped: {info}")]
Maxlag {
info: String,
retry_after: Option<u64>,
},
/// When MediaWiki is in readonly mode
#[error("MediaWiki is readonly: {info}")]
Readonly {
/// An internal MediaWiki exception
#[error("Internal MediaWiki exception: {0}")]
InternalException(ApiError),
/* Catchall/generic issues */
/// Any arbitrary error returned by the MediaWiki API
#[error("API error: {0}")]
ApiError(ApiError),
/// An error where we don't know what to do nor have
/// information to report back
#[error("Unknown error: {0}")]
Unknown(String),
impl Error {
/// Store the value of the retry-after header, if one was present, for
/// error types that are safe to retry
pub fn with_retry_after(self, value: u64) -> Self {
match self {
Error::TooManyRequests { .. } => Error::TooManyRequests {
retry_after: Some(value),
Error::Maxlag { info, .. } => Error::Maxlag {
info,
Error::Readonly { info, .. } => Error::Readonly {
err => err,
/// If the error merits a retry, how long should we wait?
pub fn retry_after(&self) -> Option<u64> {
Error::TooManyRequests { retry_after, .. } => {
Some((*retry_after).unwrap_or(1))
Error::Maxlag { retry_after, .. } => {
Error::Readonly { retry_after, .. } => {
_ => None,
impl From<ApiError> for Error {
fn from(apierr: ApiError) -> Self {
match apierr.code.as_str() {
"assertuserfailed" => Self::NotLoggedIn,
"assertbotfailed" => Self::NotLoggedInAsBot,
"badtoken" => Self::BadToken,
"maxlag" => Self::Maxlag {
info: apierr.text,
retry_after: None,
"readonly" => Self::Readonly {
code => {
if code.starts_with("internal_api_error_") {
Self::InternalException(apierr)
} else {
Self::ApiError(apierr)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_apierror() {
let apierr = ApiError {
code: "assertbotfailed".to_string(),
text: "Something something".to_string(),
data: None,
};
let err = Error::from(apierr);
assert!(matches!(err, Error::NotLoggedInAsBot));
fn test_to_string() {
code: "errorcode".to_string(),
text: "Some description".to_string(),
assert_eq!(&apierr.to_string(), "(code: errorcode): Some description");