use core::fmt;
use common::LeafError;
use http::header::{InvalidHeaderName, InvalidHeaderValue, ToStrError};
use thiserror::Error;
use crate::api::err::ApiError;
use crate::exec::Error as ExecError;
use crate::iam::PolicyError;
use crate::idx::Error as IdxError;
use crate::kvs::Error as KvsError;
#[cfg(test)]
mod behaviour_pins;
mod to_types;
#[cfg(test)]
mod wire_snapshot_test;
pub(crate) use common::EngineError;
pub(crate) use to_types::into_types_error;
macro_rules! error_registry {
($( $ty:ty {
map: $map:expr,
probe: $probe:expr,
expect: $expect:ident,
} )*) => {
pub fn anyhow_to_types_error(error: anyhow::Error) -> surrealdb_types::Error {
let error = match error.downcast::<surrealdb_types::Error>() {
Ok(e) => return e,
Err(e) => e,
};
$(
let error = match error.downcast::<$ty>() {
Ok(e) => return ($map)(e),
Err(e) => e,
};
)*
surrealdb_types::Error::from_anyhow_with_chain(error)
}
#[cfg(test)]
const REGISTERED: &[&str] = &[$( stringify!($ty) ),*];
#[cfg(test)]
mod registry_tests {
use std::collections::BTreeMap;
use super::*;
#[test]
fn every_registered_type_survives_the_anyhow_boundary() {
$({
let mapped = anyhow_to_types_error(anyhow::Error::new($probe));
assert!(
mapped.$expect(),
concat!(stringify!($ty), " lost its classification at the boundary: {}"),
mapped.kind_str(),
);
let unregistered = surrealdb_types::Error::from_anyhow_with_chain(
anyhow::Error::new($probe),
);
assert_ne!(
mapped,
unregistered,
concat!(
stringify!($ty),
"'s probe cannot distinguish being registered from not being \
registered, so it does not guard the registration",
),
);
})*
}
#[test]
fn public_errors_pass_through_verbatim() {
let original = surrealdb_types::Error::not_found("gone".to_string(), None);
assert_eq!(anyhow_to_types_error(anyhow::Error::new(original.clone())), original);
}
#[test]
fn every_leaf_error_is_registered() {
fn tally(names: impl Iterator<Item = String>) -> BTreeMap<String, usize> {
let mut counts = BTreeMap::new();
for name in names {
*counts.entry(name).or_insert(0) += 1;
}
counts
}
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let mut implemented = Vec::new();
let siblings: Vec<_> = std::fs::read_dir(root.join(".."))
.expect("read the crate directory")
.flatten()
.filter(|sibling| sibling.file_name() != "core")
.map(|sibling| sibling.path().join("src"))
.filter(|src| src.is_dir())
.collect();
let scanned: std::collections::HashSet<_> = siblings
.iter()
.filter_map(|src| src.parent()?.file_name()?.to_str().map(str::to_owned))
.collect();
for owner in ["common", "syn"] {
assert!(
scanned.contains(owner),
"`{owner}` owns a `LeafError` impl but was not scanned; the sibling walk \
covers {scanned:?}. If the crate moved, teach the walk where it went \
rather than dropping it, or its errors silently stop being checked",
);
}
let mut pending = vec![root.join("src")];
pending.extend(siblings);
while let Some(dir) = pending.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
pending.push(path);
} else if path.extension().is_some_and(|e| e == "rs") {
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
implemented.extend(text.lines().filter_map(|l| {
let l = l.trim();
let rest = l
.strip_prefix("impl LeafError for ")
.or_else(|| l.strip_prefix("impl crate::LeafError for "))
.or_else(|| l.strip_prefix("impl common::LeafError for "))?;
Some(rest.trim_end_matches(" {").to_string())
}));
}
}
}
assert!(!implemented.is_empty(), "found no `LeafError` impls; the scan is broken");
let registered = tally(
REGISTERED.iter().map(|e| e.rsplit("::").next().unwrap_or(e).to_string()),
);
let missing: Vec<_> = tally(implemented.into_iter())
.into_iter()
.filter(|(name, count)| registered.get(name).copied().unwrap_or(0) < *count)
.collect();
assert!(
missing.is_empty(),
"these types implement `LeafError` more often than the registry lists them, \
so at least one degrades to an untyped internal error at the boundary: \
{missing:?}",
);
}
#[test]
fn foreign_errors_fall_through() {
let mapped = anyhow_to_types_error(anyhow::Error::new(std::io::Error::other("disk")));
assert!(mapped.is_internal());
assert!(mapped.message().contains("disk"));
}
}
};
}
error_registry! {
Error {
map: into_types_error,
probe: Error::Http("sample".to_string()),
expect: is_connection,
}
ApiError {
map: LeafError::to_types_error,
probe: ApiError::NotFound,
expect: is_not_found,
}
KvsError {
map: |e| into_types_error(Error::Kvs(e)),
probe: KvsError::TransactionConflict("busy".to_string()),
expect: is_query,
}
PolicyError {
map: |e| into_types_error(Error::IamError(e)),
probe: PolicyError::NotAllowed {
actor: "a".to_string(),
action: "b".to_string(),
resource: "c".to_string(),
},
expect: is_not_allowed,
}
EngineError {
map: LeafError::to_types_error,
probe: EngineError::QueryCancelled,
expect: is_query,
}
crate::key::Error {
map: LeafError::to_types_error,
probe: crate::key::Error::Unencodable,
expect: is_serialization,
}
crate::expr::Error {
map: LeafError::to_types_error,
probe: crate::expr::Error::TryAdd("a".to_string(), "b".to_string()),
expect: is_validation,
}
crate::exec::Error {
map: LeafError::to_types_error,
probe: crate::exec::Error::NsEmpty,
expect: is_validation,
}
crate::catalog::Error {
map: LeafError::to_types_error,
probe: crate::catalog::Error::NsNotFound {
name: "n".to_string(),
},
expect: is_not_found,
}
crate::buc::Error {
map: LeafError::to_types_error,
probe: crate::buc::Error::ObsError(object_store::Error::NotSupported {
source: "sample".into(),
}),
expect: is_internal,
}
crate::idx::Error {
map: LeafError::to_types_error,
probe: crate::idx::Error::DuplicatedMatchRef {
mr: 1,
},
expect: is_validation,
}
crate::doc::Error {
map: LeafError::to_types_error,
probe: crate::doc::Error::IdNotFound {
rid: "r".to_string(),
},
expect: is_not_found,
}
crate::kvs::DatastoreError {
map: LeafError::to_types_error,
probe: crate::kvs::DatastoreError::ExpiredSession,
expect: is_not_allowed,
}
crate::iam::Error {
map: LeafError::to_types_error,
probe: crate::iam::Error::ExpiredToken,
expect: is_not_allowed,
}
crate::dbs::capabilities::Error {
map: LeafError::to_types_error,
probe: crate::dbs::capabilities::Error::ScriptingNotAllowed,
expect: is_not_allowed,
}
crate::dbs::SortError {
map: LeafError::to_types_error,
probe: crate::dbs::SortError::Revision(revision::Error::Serialize("sample".to_string())),
expect: is_serialization,
}
crate::syn::ParseError {
map: LeafError::to_types_error,
probe: crate::syn::ParseError::InvalidQuery(surrealdb_syn::error::RenderedError {
errors: vec!["sample".to_string()],
snippets: Vec::new(),
}),
expect: is_validation,
}
}
pub(crate) fn engine_error(error: &anyhow::Error) -> Option<&EngineError> {
if let Some(engine) = error.downcast_ref::<EngineError>() {
return Some(engine);
}
match error.downcast_ref::<Error>() {
Some(Error::Engine(engine)) => Some(engine),
_ => None,
}
}
pub(crate) fn exec_error(error: &anyhow::Error) -> Option<&ExecError> {
if let Some(exec) = error.downcast_ref::<ExecError>() {
return Some(exec);
}
match error.downcast_ref::<Error>() {
Some(Error::Exec(exec)) => Some(exec),
_ => None,
}
}
pub fn is_query_cancelled(error: &anyhow::Error) -> bool {
matches!(engine_error(error), Some(EngineError::QueryCancelled))
}
pub fn is_query_timedout(error: &anyhow::Error) -> bool {
matches!(engine_error(error), Some(EngineError::QueryTimedout(_)))
}
#[derive(Error, Debug)]
#[allow(
clippy::enum_variant_names,
reason = "each wrapper is named after the layer error it holds, two of which end in `Error`"
)]
#[cfg_attr(
not(any(feature = "http", feature = "jwks")),
allow(dead_code, reason = "the outbound HTTP variants need a client to be compiled in")
)]
pub(crate) enum Error {
#[error(transparent)]
Engine(#[from] EngineError),
#[error(transparent)]
Exec(#[from] ExecError),
#[error(transparent)]
Idx(#[from] IdxError),
#[error("There was a problem with the key-value store: {0}")]
Kvs(#[from] KvsError),
#[error("IAM error: {0}")]
IamError(#[from] PolicyError),
#[error(transparent)]
ApiError(ApiError),
#[error("The string could not be parsed into a path: {0}")]
InvalidPath(String),
#[error("The URL `{0}` is invalid")]
InvalidUrl(String),
#[error("There was an error processing a remote HTTP request: {0}")]
Http(String),
}
impl Error {
#[cold]
#[track_caller]
pub fn unreachable<T: fmt::Display>(message: T) -> Error {
let location = std::panic::Location::caller();
let message = format!("{}:{}: {}", location.file(), location.line(), message);
EngineError::Unreachable(message).into()
}
}
impl From<Error> for String {
fn from(e: Error) -> String {
e.to_string()
}
}
impl From<ApiError> for Error {
fn from(value: ApiError) -> Self {
Error::ApiError(value)
}
}
impl From<InvalidHeaderName> for Error {
fn from(error: InvalidHeaderName) -> Self {
EngineError::Unreachable(error.to_string()).into()
}
}
impl From<InvalidHeaderValue> for Error {
fn from(error: InvalidHeaderValue) -> Self {
EngineError::Unreachable(error.to_string()).into()
}
}
impl From<ToStrError> for Error {
fn from(error: ToStrError) -> Self {
EngineError::Unreachable(error.to_string()).into()
}
}
#[cfg(any(feature = "http", feature = "jwks"))]
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Error {
Error::Http(e.to_string())
}
}
impl From<Error> for crate::expr::ControlFlow {
fn from(error: Error) -> Self {
crate::expr::ControlFlow::Err(error.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::iam::Error as AuthError;
#[test]
fn test_anyhow_to_types_error_signup_query_failed() {
let error = anyhow::Error::new(AuthError::AccessRecordSignupQueryFailed);
let types_error = anyhow_to_types_error(error);
assert!(
types_error.is_query(),
"expected Query error, got {} with message: {}",
types_error.kind_str(),
types_error.message()
);
assert!(!types_error.is_not_allowed(), "a failing SIGNUP clause is not an auth refusal");
}
#[test]
fn test_anyhow_to_types_error_signin_query_failed() {
let error = anyhow::Error::new(AuthError::AccessRecordSigninQueryFailed);
let types_error = anyhow_to_types_error(error);
assert!(
types_error.is_query(),
"expected Query error, got {} with message: {}",
types_error.kind_str(),
types_error.message()
);
assert!(!types_error.is_not_allowed(), "a failing SIGNIN clause is not an auth refusal");
}
}