use std::io::ErrorKind as IoKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
SourceNotFound,
SourceTooLarge,
SourceUnreadable,
Upstream,
SourceRejected,
UpstreamTimeout,
Undecodable,
Internal,
}
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
inner: anyhow::Error,
}
impl Error {
pub fn kind(&self) -> ErrorKind {
self.kind
}
pub(crate) fn classify(inner: anyhow::Error, remote: bool) -> Error {
let kind = if remote && chain_has_timeout(&inner) {
ErrorKind::UpstreamTimeout
} else if let Some(io) = inner.downcast_ref::<std::io::Error>() {
match io.kind() {
IoKind::NotFound => ErrorKind::SourceNotFound,
IoKind::FileTooLarge => ErrorKind::SourceTooLarge,
IoKind::PermissionDenied | IoKind::IsADirectory => ErrorKind::SourceUnreadable,
IoKind::ConnectionReset
| IoKind::ConnectionAborted
| IoKind::BrokenPipe
| IoKind::UnexpectedEof
if remote =>
{
ErrorKind::Upstream
}
_ => ErrorKind::Undecodable,
}
} else if inner.downcast_ref::<super::SourceRejected>().is_some() {
ErrorKind::SourceRejected
} else if inner.downcast_ref::<super::UpstreamFault>().is_some() {
ErrorKind::Upstream
} else if inner.downcast_ref::<super::ServerFault>().is_some() {
ErrorKind::Internal
} else {
ErrorKind::Undecodable
};
Error { kind, inner }
}
}
fn chain_has_timeout(inner: &anyhow::Error) -> bool {
inner.chain().any(|c| {
c.downcast_ref::<std::io::Error>()
.is_some_and(|io| io.kind() == IoKind::TimedOut)
})
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
write!(f, "{:#}", self.inner)
} else {
write!(f, "{}", self.inner)
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.inner.chain().nth(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn io(kind: IoKind) -> anyhow::Error {
anyhow::Error::new(std::io::Error::new(kind, "io-level detail"))
}
#[test]
fn classification_table() {
for (err, remote, want) in [
(io(IoKind::NotFound), false, ErrorKind::SourceNotFound),
(io(IoKind::NotFound), true, ErrorKind::SourceNotFound),
(io(IoKind::FileTooLarge), false, ErrorKind::SourceTooLarge),
(io(IoKind::FileTooLarge), true, ErrorKind::SourceTooLarge),
(
io(IoKind::PermissionDenied),
false,
ErrorKind::SourceUnreadable,
),
(io(IoKind::IsADirectory), false, ErrorKind::SourceUnreadable),
(io(IoKind::UnexpectedEof), true, ErrorKind::Upstream),
(io(IoKind::UnexpectedEof), false, ErrorKind::Undecodable),
(io(IoKind::ConnectionReset), true, ErrorKind::Upstream),
(io(IoKind::TimedOut), true, ErrorKind::UpstreamTimeout),
(io(IoKind::TimedOut), false, ErrorKind::Undecodable),
(
io(IoKind::TimedOut).context("decode png"),
true,
ErrorKind::UpstreamTimeout,
),
(
anyhow::anyhow!("origin 500").context(super::super::UpstreamFault),
false,
ErrorKind::Upstream,
),
(
anyhow::anyhow!("worker died").context(super::super::ServerFault),
false,
ErrorKind::Internal,
),
(
anyhow::anyhow!("key too long").context(super::super::SourceRejected),
true,
ErrorKind::SourceRejected,
),
(
anyhow::anyhow!("origin 414").context(super::super::SourceRejected),
false,
ErrorKind::SourceRejected,
),
(
anyhow::anyhow!("bogus bytes"),
false,
ErrorKind::Undecodable,
),
(anyhow::anyhow!("bogus bytes"), true, ErrorKind::Undecodable),
] {
assert_eq!(Error::classify(err, remote).kind(), want, "remote={remote}");
}
}
#[test]
fn classification_sees_through_context() {
let err = io(IoKind::NotFound).context("open source");
assert_eq!(
Error::classify(err, false).kind(),
ErrorKind::SourceNotFound
);
}
#[test]
fn display_forms_and_source() {
let e = Error::classify(io(IoKind::NotFound).context("open source"), false);
assert_eq!(format!("{e}"), "open source");
assert_eq!(format!("{e:#}"), "open source: io-level detail");
let src = std::error::Error::source(&e).expect("has a source");
assert_eq!(src.to_string(), "io-level detail");
}
}