use std::fmt;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorScope {
Egress,
Provider,
Schema,
Query,
Internal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
RateLimited { retry_after: Option<Duration> },
Blocked(BlockDetails),
MalformedPayload { context: &'static str },
UpstreamUnavailable { status: u16 },
AllProvidersFailed { details: Vec<String> },
Timeout,
NetworkFailure,
InvalidQuery { context: &'static str },
Internal { context: &'static str },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlockDetails {
Cloudflare,
Captcha,
IpBan,
BotDetection,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
pub scope: ErrorScope,
pub kind: ErrorKind,
pub engine: &'static str,
pub http_status: Option<u16>,
pub message: Option<&'static str>,
}
impl Error {
pub fn rate_limited(engine: &'static str, retry_after: Option<Duration>) -> Self {
Self {
scope: ErrorScope::Provider,
kind: ErrorKind::RateLimited { retry_after },
engine,
http_status: Some(429),
message: None,
}
}
pub fn blocked(engine: &'static str, details: BlockDetails) -> Self {
Self {
scope: ErrorScope::Egress,
kind: ErrorKind::Blocked(details),
engine,
http_status: None,
message: None,
}
}
pub fn schema(engine: &'static str, context: &'static str) -> Self {
Self {
scope: ErrorScope::Schema,
kind: ErrorKind::MalformedPayload { context },
engine,
http_status: None,
message: None,
}
}
pub fn unavailable(engine: &'static str, status: u16) -> Self {
Self {
scope: ErrorScope::Provider,
kind: ErrorKind::UpstreamUnavailable { status },
engine,
http_status: Some(status),
message: None,
}
}
pub fn timeout(engine: &'static str) -> Self {
Self {
scope: ErrorScope::Egress,
kind: ErrorKind::Timeout,
engine,
http_status: None,
message: None,
}
}
pub fn network(engine: &'static str) -> Self {
Self {
scope: ErrorScope::Egress,
kind: ErrorKind::NetworkFailure,
engine,
http_status: None,
message: None,
}
}
pub fn invalid_query(engine: &'static str, context: &'static str) -> Self {
Self {
scope: ErrorScope::Query,
kind: ErrorKind::InvalidQuery { context },
engine,
http_status: None,
message: None,
}
}
pub fn internal(engine: &'static str, context: &'static str) -> Self {
Self {
scope: ErrorScope::Internal,
kind: ErrorKind::Internal { context },
engine,
http_status: None,
message: None,
}
}
pub fn all_failed(engine: &'static str, details: Vec<String>) -> Self {
Self {
scope: ErrorScope::Provider,
kind: ErrorKind::AllProvidersFailed { details },
engine,
http_status: None,
message: None,
}
}
pub fn scope(&self) -> ErrorScope {
self.scope
}
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorKind::RateLimited { retry_after } => match retry_after {
Some(d) => write!(f, "rate limited (retry after {}s)", d.as_secs()),
None => write!(f, "rate limited"),
},
ErrorKind::Blocked(d) => write!(f, "blocked ({d:?})"),
ErrorKind::MalformedPayload { context } => write!(f, "malformed payload: {context}"),
ErrorKind::UpstreamUnavailable { status } => {
write!(f, "upstream unavailable (status {status})")
}
ErrorKind::AllProvidersFailed { details } => {
if details.is_empty() {
write!(f, "all search providers failed")
} else {
write!(f, "all search providers failed: {}", details.join("; "))
}
}
ErrorKind::Timeout => write!(f, "timeout"),
ErrorKind::NetworkFailure => write!(f, "network failure"),
ErrorKind::InvalidQuery { context } => write!(f, "invalid query: {context}"),
ErrorKind::Internal { context } => write!(f, "internal error: {context}"),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
write!(f, " [scope={:?}, engine={}]", self.scope, self.engine)?;
if let Some(status) = self.http_status {
write!(f, " [status={status}]")?;
}
if let Some(m) = self.message {
write!(f, ": {m}")?;
}
Ok(())
}
}
impl std::error::Error for Error {}
impl From<std::io::Error> for Error {
fn from(_e: std::io::Error) -> Self {
Error::internal("io", "i/o failure")
}
}
impl From<wreq::Error> for Error {
fn from(e: wreq::Error) -> Self {
if e.is_timeout() {
Error::timeout("client")
} else if e.is_connect() || e.is_connection_reset() {
Error::network("client")
} else if e.is_redirect() || e.is_decode() {
Error::internal("client", "request failed")
} else {
Error::network("client")
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scope_and_kind_classify() {
let e = Error::rate_limited("qwant", Some(Duration::from_secs(30)));
assert_eq!(e.scope(), ErrorScope::Provider);
assert_eq!(
e.kind(),
&ErrorKind::RateLimited {
retry_after: Some(Duration::from_secs(30))
}
);
assert_eq!(e.http_status, Some(429));
let e = Error::blocked("google", BlockDetails::BotDetection);
assert_eq!(e.scope(), ErrorScope::Egress);
assert_eq!(e.kind(), &ErrorKind::Blocked(BlockDetails::BotDetection));
let e = Error::schema("bing", "unexpected content-type");
assert_eq!(e.scope(), ErrorScope::Schema);
let e = Error::invalid_query("orchestrator", "no engines");
assert_eq!(e.scope(), ErrorScope::Query);
let e = Error::internal("client", "build failed");
assert_eq!(e.scope(), ErrorScope::Internal);
}
#[test]
fn display_is_readable() {
let e = Error::unavailable("mojeek", 503);
let s = e.to_string();
assert!(s.contains("upstream unavailable"));
assert!(s.contains("mojeek"));
assert!(s.contains("503"));
let e = Error::blocked("google", BlockDetails::Cloudflare);
assert!(e.to_string().contains("blocked (Cloudflare)"));
}
}