Anki/rslib/src/error/network.rs
RumovZ c521753057
Refactor error handling (#2136)
* Add crate snafu

* Replace all inline structs in AnkiError

* Derive Snafu on AnkiError

* Use snafu for card type errors

* Use snafu whatever error for InvalidInput

* Use snafu for NotFoundError and improve message

* Use snafu for FileIoError to attach context

Remove IoError.
Add some context-attaching helpers to replace code returning bare
io::Errors.

* Add more context-attaching io helpers

* Add message, context and backtrace to new snafus

* Utilize error context and backtrace on frontend

* Rename LocalizedError -> BackendError.
* Remove DocumentedError.
* Have all backend exceptions inherit BackendError.

* Rename localized(_description) -> message

* Remove accidentally committed experimental trait

* invalid_input_context -> ok_or_invalid

* ensure_valid_input! -> require!

* Always return `Err` from `invalid_input!`

Instead of a Result to unwrap, the macro accepts a source error now.

* new_tempfile_in_parent -> new_tempfile_in_parent_of

* ok_or_not_found -> or_not_found

* ok_or_invalid -> or_invalid

* Add crate convert_case

* Use unqualified lowercase type name

* Remove uses of snafu::ensure

* Allow public construction of InvalidInputErrors (dae)

Needed to port the AnkiDroid changes.

* Make into_protobuf() public (dae)

Also required for AnkiDroid. Not sure why it worked previously - possible
bug in older Rust version?
2022-10-21 18:02:12 +10:00

194 lines
5.8 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anki_i18n::I18n;
use reqwest::StatusCode;
use snafu::Snafu;
use super::AnkiError;
#[derive(Debug, PartialEq, Eq, Snafu)]
pub struct NetworkError {
pub info: String,
pub kind: NetworkErrorKind,
}
#[derive(Debug, PartialEq, Eq)]
pub enum NetworkErrorKind {
Offline,
Timeout,
ProxyAuth,
Other,
}
#[derive(Debug, PartialEq, Eq, Snafu)]
pub struct SyncError {
pub info: String,
pub kind: SyncErrorKind,
}
#[derive(Debug, PartialEq, Eq)]
pub enum SyncErrorKind {
Conflict,
ServerError,
ClientTooOld,
AuthFailed,
ServerMessage,
ClockIncorrect,
Other,
ResyncRequired,
DatabaseCheckRequired,
SyncNotStarted,
UploadTooLarge,
}
impl AnkiError {
pub(crate) fn sync_error(info: impl Into<String>, kind: SyncErrorKind) -> Self {
AnkiError::SyncError {
source: SyncError {
info: info.into(),
kind,
},
}
}
pub(crate) fn server_message<S: Into<String>>(msg: S) -> AnkiError {
AnkiError::sync_error(msg, SyncErrorKind::ServerMessage)
}
}
impl From<reqwest::Error> for AnkiError {
fn from(err: reqwest::Error) -> Self {
let url = err.url().map(|url| url.as_str()).unwrap_or("");
let str_err = format!("{}", err);
// strip url from error to avoid exposing keys
let info = str_err.replace(url, "");
if err.is_timeout() {
AnkiError::NetworkError {
source: NetworkError {
info,
kind: NetworkErrorKind::Timeout,
},
}
} else if err.is_status() {
error_for_status_code(info, err.status().unwrap())
} else {
guess_reqwest_error(info)
}
}
}
fn error_for_status_code(info: String, code: StatusCode) -> AnkiError {
use reqwest::StatusCode as S;
match code {
S::PROXY_AUTHENTICATION_REQUIRED => AnkiError::NetworkError {
source: NetworkError {
info,
kind: NetworkErrorKind::ProxyAuth,
},
},
S::CONFLICT => AnkiError::SyncError {
source: SyncError {
info,
kind: SyncErrorKind::Conflict,
},
},
S::FORBIDDEN => AnkiError::SyncError {
source: SyncError {
info,
kind: SyncErrorKind::AuthFailed,
},
},
S::NOT_IMPLEMENTED => AnkiError::SyncError {
source: SyncError {
info,
kind: SyncErrorKind::ClientTooOld,
},
},
S::INTERNAL_SERVER_ERROR | S::BAD_GATEWAY | S::GATEWAY_TIMEOUT | S::SERVICE_UNAVAILABLE => {
AnkiError::SyncError {
source: SyncError {
info,
kind: SyncErrorKind::ServerError,
},
}
}
S::BAD_REQUEST => AnkiError::SyncError {
source: SyncError {
info,
kind: SyncErrorKind::DatabaseCheckRequired,
},
},
_ => AnkiError::NetworkError {
source: NetworkError {
info,
kind: NetworkErrorKind::Other,
},
},
}
}
fn guess_reqwest_error(mut info: String) -> AnkiError {
if info.contains("dns error: cancelled") {
return AnkiError::Interrupted;
}
let kind = if info.contains("unreachable") || info.contains("dns") {
NetworkErrorKind::Offline
} else if info.contains("timed out") {
NetworkErrorKind::Timeout
} else {
if info.contains("invalid type") {
info = format!(
"{} {} {}\n\n{}",
"Please force a full sync in the Preferences screen to bring your devices into sync.",
"Then, please use the Check Database feature, and sync to your other devices.",
"If problems persist, please post on the support forum.",
info,
);
}
NetworkErrorKind::Other
};
AnkiError::NetworkError {
source: NetworkError { info, kind },
}
}
impl From<zip::result::ZipError> for AnkiError {
fn from(err: zip::result::ZipError) -> Self {
AnkiError::sync_error(err.to_string(), SyncErrorKind::Other)
}
}
impl SyncError {
pub fn message(&self, tr: &I18n) -> String {
match self.kind {
SyncErrorKind::ServerMessage => self.info.clone().into(),
SyncErrorKind::Other => self.info.clone().into(),
SyncErrorKind::Conflict => tr.sync_conflict(),
SyncErrorKind::ServerError => tr.sync_server_error(),
SyncErrorKind::ClientTooOld => tr.sync_client_too_old(),
SyncErrorKind::AuthFailed => tr.sync_wrong_pass(),
SyncErrorKind::ResyncRequired => tr.sync_resync_required(),
SyncErrorKind::ClockIncorrect => tr.sync_clock_off(),
SyncErrorKind::DatabaseCheckRequired => tr.sync_sanity_check_failed(),
SyncErrorKind::SyncNotStarted => "sync not started".into(),
SyncErrorKind::UploadTooLarge => tr.sync_upload_too_large(&self.info),
}
.into()
}
}
impl NetworkError {
pub fn message(&self, tr: &I18n) -> String {
let summary = match self.kind {
NetworkErrorKind::Offline => tr.network_offline(),
NetworkErrorKind::Timeout => tr.network_timeout(),
NetworkErrorKind::ProxyAuth => tr.network_proxy_auth(),
NetworkErrorKind::Other => tr.network_other(),
};
let details = tr.network_details(self.info.as_str());
format!("{}\n\n{}", summary, details)
}
}