use thiserror::Error;
pub mod database;
pub mod param_error;
pub use database::{DatabaseError, DatabaseErrorKind};
pub use param_error::{ParamErrorContext, ParamType};
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum Error {
#[error("HTTP error: {0}")]
Http(String),
#[error("Database error: {0}")]
Database(#[from] DatabaseError),
#[error("Database error: {database_error}")]
DatabaseWithSource {
database_error: DatabaseError,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Authentication error: {0}")]
Authentication(String),
#[error("Authorization error: {0}")]
Authorization(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Template not found: {0}")]
TemplateNotFound(String),
#[error("Method not allowed: {0}")]
MethodNotAllowed(String),
#[error("Conflict: {0}")]
Conflict(String),
#[error("Internal server error: {0}")]
Internal(String),
#[error("Improperly configured: {0}")]
ImproperlyConfigured(String),
#[error("Body already consumed")]
BodyAlreadyConsumed,
#[error("Parse error: {0}")]
ParseError(String),
#[error("Missing Content-Type header")]
MissingContentType,
#[error("Invalid page: {0}")]
InvalidPage(String),
#[error("Invalid cursor: {0}")]
InvalidCursor(String),
#[error("Invalid limit: {0}")]
InvalidLimit(String),
#[error("Missing parameter: {0}")]
MissingParameter(String),
#[error("{}", .0.format_error())]
ParamValidation(Box<ParamErrorContext>),
}
pub type Result<T> = std::result::Result<T, Error>;
pub use http::StatusCode;
pub trait HttpError {
fn status_code(&self) -> StatusCode;
fn client_message(&self) -> std::borrow::Cow<'static, str>;
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorKind {
Http,
Database,
Serialization,
Validation,
Authentication,
Authorization,
NotFound,
MethodNotAllowed,
Conflict,
Internal,
ImproperlyConfigured,
BodyAlreadyConsumed,
Parse,
ParamValidation,
}
impl Error {
pub fn status_code(&self) -> u16 {
match self {
Error::Http(_) => 400,
Error::Database(database_error) | Error::DatabaseWithSource { database_error, .. } => {
match database_error.kind() {
DatabaseErrorKind::UniqueViolation | DatabaseErrorKind::ForeignKeyViolation => {
409
}
DatabaseErrorKind::NotNullViolation | DatabaseErrorKind::CheckViolation => 400,
DatabaseErrorKind::Connection | DatabaseErrorKind::Timeout => 503,
DatabaseErrorKind::Syntax
| DatabaseErrorKind::Type
| DatabaseErrorKind::ColumnNotFound
| DatabaseErrorKind::ConnectionHandleExpired
| DatabaseErrorKind::Transaction
| DatabaseErrorKind::Configuration
| DatabaseErrorKind::Serialization
| DatabaseErrorKind::Unsupported
| DatabaseErrorKind::Query => 500,
}
}
Error::Serialization(_) => 400,
Error::Validation(_) => 400,
Error::Authentication(_) => 401,
Error::Authorization(_) => 403,
Error::NotFound(_) => 404,
Error::TemplateNotFound(_) => 404,
Error::MethodNotAllowed(_) => 405,
Error::Conflict(_) => 409,
Error::Internal(_) => 500,
Error::ImproperlyConfigured(_) => 500,
Error::BodyAlreadyConsumed => 400,
Error::ParseError(_) => 400,
Error::MissingContentType => 400,
Error::InvalidPage(_) => 400,
Error::InvalidCursor(_) => 400,
Error::InvalidLimit(_) => 400,
Error::MissingParameter(_) => 400,
Error::ParamValidation(_) => 400,
}
}
pub fn kind(&self) -> ErrorKind {
match self {
Error::Http(_) => ErrorKind::Http,
Error::Database(_) | Error::DatabaseWithSource { .. } => ErrorKind::Database,
Error::Serialization(_) => ErrorKind::Serialization,
Error::Validation(_) => ErrorKind::Validation,
Error::Authentication(_) => ErrorKind::Authentication,
Error::Authorization(_) => ErrorKind::Authorization,
Error::NotFound(_) => ErrorKind::NotFound,
Error::TemplateNotFound(_) => ErrorKind::NotFound,
Error::MethodNotAllowed(_) => ErrorKind::MethodNotAllowed,
Error::Conflict(_) => ErrorKind::Conflict,
Error::Internal(_) => ErrorKind::Internal,
Error::ImproperlyConfigured(_) => ErrorKind::ImproperlyConfigured,
Error::BodyAlreadyConsumed => ErrorKind::BodyAlreadyConsumed,
Error::ParseError(_) => ErrorKind::Parse,
Error::MissingContentType => ErrorKind::Http,
Error::InvalidPage(_) => ErrorKind::Validation,
Error::InvalidCursor(_) => ErrorKind::Validation,
Error::InvalidLimit(_) => ErrorKind::Validation,
Error::MissingParameter(_) => ErrorKind::Validation,
Error::ParamValidation(_) => ErrorKind::ParamValidation,
}
}
pub fn database_error(&self) -> Option<&DatabaseError> {
match self {
Self::Database(error)
| Self::DatabaseWithSource {
database_error: error,
..
} => Some(error),
_ => None,
}
}
pub fn database_kind(&self) -> Option<DatabaseErrorKind> {
self.database_error().map(DatabaseError::kind)
}
pub fn database_with_source(
kind: DatabaseErrorKind,
message: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::DatabaseWithSource {
database_error: DatabaseError::new(kind, message),
source: Box::new(source),
}
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Error::Serialization(err.to_string())
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Internal(format!("IO error: {}", err))
}
}
impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
Error::Http(err.to_string())
}
}
impl From<String> for Error {
fn from(msg: String) -> Self {
Error::Internal(msg)
}
}
impl From<&str> for Error {
fn from(msg: &str) -> Self {
Error::Internal(msg.to_string())
}
}
#[cfg(feature = "validators")]
impl From<crate::validators::ValidationErrors> for Error {
fn from(err: crate::validators::ValidationErrors) -> Self {
Error::Validation(format!("Validation failed: {}", err))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, thiserror::Error)]
#[error("test source")]
struct TestSourceError;
#[test]
fn test_error_kind_mapping() {
assert_eq!(Error::Http("test".to_string()).kind(), ErrorKind::Http);
assert_eq!(Error::MissingContentType.kind(), ErrorKind::Http);
assert_eq!(
Error::from(DatabaseError::new(DatabaseErrorKind::Query, "test")).kind(),
ErrorKind::Database
);
assert_eq!(
Error::Serialization("test".to_string()).kind(),
ErrorKind::Serialization
);
assert_eq!(
Error::Validation("test".to_string()).kind(),
ErrorKind::Validation
);
assert_eq!(
Error::InvalidPage("test".to_string()).kind(),
ErrorKind::Validation
);
assert_eq!(
Error::InvalidCursor("test".to_string()).kind(),
ErrorKind::Validation
);
assert_eq!(
Error::InvalidLimit("test".to_string()).kind(),
ErrorKind::Validation
);
assert_eq!(
Error::MissingParameter("test".to_string()).kind(),
ErrorKind::Validation
);
assert_eq!(
Error::Authentication("test".to_string()).kind(),
ErrorKind::Authentication
);
assert_eq!(
Error::Authorization("test".to_string()).kind(),
ErrorKind::Authorization
);
assert_eq!(
Error::NotFound("test".to_string()).kind(),
ErrorKind::NotFound
);
assert_eq!(
Error::TemplateNotFound("test".to_string()).kind(),
ErrorKind::NotFound
);
assert_eq!(
Error::MethodNotAllowed("test".to_string()).kind(),
ErrorKind::MethodNotAllowed
);
assert_eq!(
Error::Internal("test".to_string()).kind(),
ErrorKind::Internal
);
assert_eq!(
Error::ImproperlyConfigured("test".to_string()).kind(),
ErrorKind::ImproperlyConfigured
);
assert_eq!(
Error::BodyAlreadyConsumed.kind(),
ErrorKind::BodyAlreadyConsumed
);
assert_eq!(
Error::ParseError("test".to_string()).kind(),
ErrorKind::Parse
);
}
#[test]
fn test_database_error_with_source_preserves_classification_and_source() {
let error = Error::database_with_source(
DatabaseErrorKind::Serialization,
"typed field codec failed: test source",
TestSourceError,
);
assert_eq!(error.kind(), ErrorKind::Database);
assert_eq!(
error.database_kind(),
Some(DatabaseErrorKind::Serialization)
);
assert_eq!(error.status_code(), 500);
assert_eq!(
error.to_string(),
"Database error: typed field codec failed: test source"
);
let source = std::error::Error::source(&error)
.expect("database error should preserve its typed source");
assert!(source.downcast_ref::<TestSourceError>().is_some());
}
#[test]
fn test_from_serde_json_error() {
let json_error = serde_json::from_str::<i32>("invalid").unwrap_err();
let error: Error = json_error.into();
assert_eq!(error.status_code(), 400);
assert_eq!(error.kind(), ErrorKind::Serialization);
assert!(error.to_string().contains("Serialization error"));
}
#[test]
fn test_from_io_error() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let error: Error = io_error.into();
assert_eq!(error.status_code(), 500);
assert_eq!(error.kind(), ErrorKind::Internal);
assert!(error.to_string().contains("IO error"));
}
#[test]
fn test_status_codes_comprehensive() {
assert_eq!(Error::Http("test".to_string()).status_code(), 400);
assert_eq!(Error::Serialization("test".to_string()).status_code(), 400);
assert_eq!(Error::Validation("test".to_string()).status_code(), 400);
assert_eq!(Error::BodyAlreadyConsumed.status_code(), 400);
assert_eq!(Error::ParseError("test".to_string()).status_code(), 400);
assert_eq!(Error::MissingContentType.status_code(), 400);
assert_eq!(Error::InvalidPage("test".to_string()).status_code(), 400);
assert_eq!(Error::InvalidCursor("test".to_string()).status_code(), 400);
assert_eq!(Error::InvalidLimit("test".to_string()).status_code(), 400);
assert_eq!(
Error::MissingParameter("test".to_string()).status_code(),
400
);
assert_eq!(Error::Authentication("test".to_string()).status_code(), 401);
assert_eq!(Error::Authorization("test".to_string()).status_code(), 403);
assert_eq!(Error::NotFound("test".to_string()).status_code(), 404);
assert_eq!(
Error::TemplateNotFound("test".to_string()).status_code(),
404
);
assert_eq!(
Error::MethodNotAllowed("test".to_string()).status_code(),
405
);
assert_eq!(
Error::from(DatabaseError::new(DatabaseErrorKind::Query, "test")).status_code(),
500
);
assert_eq!(Error::Internal("test".to_string()).status_code(), 500);
assert_eq!(
Error::ImproperlyConfigured("test".to_string()).status_code(),
500
);
}
#[test]
fn test_template_not_found_error() {
let error = Error::TemplateNotFound("index.html".to_string());
assert_eq!(error.status_code(), 404);
assert_eq!(error.kind(), ErrorKind::NotFound);
assert!(error.to_string().contains("Template not found"));
assert!(error.to_string().contains("index.html"));
}
#[test]
fn test_pagination_errors() {
let page_error = Error::InvalidPage("page must be positive".to_string());
assert_eq!(page_error.status_code(), 400);
assert_eq!(page_error.kind(), ErrorKind::Validation);
let cursor_error = Error::InvalidCursor("invalid base64".to_string());
assert_eq!(cursor_error.status_code(), 400);
assert_eq!(cursor_error.kind(), ErrorKind::Validation);
let limit_error = Error::InvalidLimit("limit too large".to_string());
assert_eq!(limit_error.status_code(), 400);
assert_eq!(limit_error.kind(), ErrorKind::Validation);
}
#[test]
fn test_missing_parameter_error() {
let error = Error::MissingParameter("user_id".to_string());
assert_eq!(error.status_code(), 400);
assert_eq!(error.kind(), ErrorKind::Validation);
assert!(error.to_string().contains("Missing parameter"));
assert!(error.to_string().contains("user_id"));
}
}