pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("not found: {uri}")]
NotFound {
uri: String,
},
#[error("not modified: {uri}")]
NotModified {
uri: String,
},
#[error("authentication failed: {message}")]
Unauthenticated {
message: String,
},
#[error("permission denied: {message}")]
PermissionDenied {
message: String,
},
#[error("rate limited; retry after {retry_after_ms}ms")]
RateLimited {
retry_after_ms: u64,
},
#[error("HTTP {status}: {message}")]
Http {
status: u16,
message: String,
},
#[error("invalid URL: {message}")]
InvalidUrl {
message: String,
},
#[error("crawl failed [{source_kind}]: {message}")]
Crawl {
source_kind: &'static str,
message: String,
},
#[error("extraction failed: {message}")]
Extract {
message: String,
},
#[error("embedding failed: {message}")]
Embed {
message: String,
},
#[error("index format error: {message}")]
Format {
message: String,
},
#[error("dimension mismatch: index={index}, query={query}")]
DimMismatch {
index: usize,
query: usize,
},
#[error("operation timed out after {duration_ms}ms")]
Timeout {
duration_ms: u64,
},
#[error("{backend} error: {message}")]
Backend {
backend: &'static str,
message: String,
},
#[error("I/O error: {source}")]
Io {
#[from]
source: std::io::Error,
},
}
impl From<graph_r::Error> for Error {
fn from(e: graph_r::Error) -> Self {
match e {
graph_r::Error::Io { source } => Self::Io { source },
other => Self::format(other.to_string()),
}
}
}
impl Error {
#[must_use]
pub fn not_found(uri: impl Into<String>) -> Self {
Self::NotFound { uri: uri.into() }
}
#[must_use]
pub fn not_modified(uri: impl Into<String>) -> Self {
Self::NotModified { uri: uri.into() }
}
#[must_use]
pub fn unauthenticated(message: impl Into<String>) -> Self {
Self::Unauthenticated {
message: message.into(),
}
}
#[must_use]
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::PermissionDenied {
message: message.into(),
}
}
#[must_use]
pub fn rate_limited(retry_after_ms: u64) -> Self {
Self::RateLimited { retry_after_ms }
}
#[must_use]
pub fn http(status: u16, message: impl Into<String>) -> Self {
Self::Http {
status,
message: message.into(),
}
}
#[must_use]
pub fn invalid_url(message: impl Into<String>) -> Self {
Self::InvalidUrl {
message: message.into(),
}
}
#[must_use]
pub fn crawl(source_kind: &'static str, message: impl Into<String>) -> Self {
Self::Crawl {
source_kind,
message: message.into(),
}
}
#[must_use]
pub fn extract(message: impl Into<String>) -> Self {
Self::Extract {
message: message.into(),
}
}
#[must_use]
pub fn embed(message: impl Into<String>) -> Self {
Self::Embed {
message: message.into(),
}
}
#[must_use]
pub fn format(message: impl Into<String>) -> Self {
Self::Format {
message: message.into(),
}
}
#[must_use]
pub fn backend(backend: &'static str, message: impl Into<String>) -> Self {
Self::Backend {
backend,
message: message.into(),
}
}
#[must_use]
pub fn is_retriable(&self) -> bool {
match self {
Self::Timeout { .. } | Self::RateLimited { .. } => true,
Self::Http { status, .. } => objstore_r::net::is_retriable_status(*status),
Self::Io { source } => matches!(
source.kind(),
std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::TimedOut
| std::io::ErrorKind::Interrupted
| std::io::ErrorKind::WouldBlock
),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructors_build_expected_variants() {
assert!(matches!(Error::not_found("x"), Error::NotFound { .. }));
assert!(matches!(
Error::http(503, "x"),
Error::Http { status: 503, .. }
));
assert!(matches!(
Error::backend("reqwest", "boom"),
Error::Backend {
backend: "reqwest",
..
}
));
}
#[test]
fn retriable_classification() {
assert!(Error::rate_limited(100).is_retriable());
assert!(Error::Timeout { duration_ms: 1 }.is_retriable());
assert!(Error::http(503, "").is_retriable());
assert!(Error::http(500, "").is_retriable());
assert!(!Error::http(404, "").is_retriable());
assert!(!Error::not_found("x").is_retriable());
assert!(!Error::unauthenticated("x").is_retriable());
}
#[test]
fn io_errors_convert_via_from() {
let io = std::io::Error::new(std::io::ErrorKind::TimedOut, "slow");
let err: Error = io.into();
assert!(err.is_retriable());
assert!(matches!(err, Error::Io { .. }));
}
#[test]
fn display_is_human_readable() {
assert_eq!(
Error::not_found("https://x/y").to_string(),
"not found: https://x/y"
);
}
}