use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use std::fmt;
#[derive(Debug)]
pub enum HttpError {
BadRequest(String),
Unauthorized(String),
Forbidden(String),
NotFound(String),
Conflict(String),
InternalError(String),
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HttpError::BadRequest(msg) => write!(f, "Bad Request: {}", msg),
HttpError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
HttpError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
HttpError::NotFound(msg) => write!(f, "Not Found: {}", msg),
HttpError::Conflict(msg) => write!(f, "Conflict: {}", msg),
HttpError::InternalError(msg) => write!(f, "Internal Error: {}", msg),
}
}
}
impl std::error::Error for HttpError {}
impl IntoResponse for HttpError {
fn into_response(self) -> Response {
let (status, message) = match self {
HttpError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
HttpError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
HttpError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg),
HttpError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
HttpError::Conflict(msg) => (StatusCode::CONFLICT, msg),
HttpError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
};
let body = Json(json!({
"error": message,
"status": status.as_u16(),
}));
(status, body).into_response()
}
}
impl HttpError {
pub fn bad_request<T: Into<String>>(msg: T) -> Self {
HttpError::BadRequest(msg.into())
}
pub fn not_found<T: Into<String>>(msg: T) -> Self {
HttpError::NotFound(msg.into())
}
pub fn unauthorized<T: Into<String>>(msg: T) -> Self {
HttpError::Unauthorized(msg.into())
}
pub fn forbidden<T: Into<String>>(msg: T) -> Self {
HttpError::Forbidden(msg.into())
}
pub fn internal<T: Into<String>>(msg: T) -> Self {
HttpError::InternalError(msg.into())
}
pub fn transaction_message<T: Into<String>>(message: T) -> Self {
let message = message.into();
if message.contains("[transaction_not_found]") {
Self::NotFound(message)
} else if message.contains("[transaction_not_owner]") {
Self::Unauthorized(message)
} else if message.contains("[write_transaction_conflict]") {
Self::Conflict(message)
} else if message.contains("[transaction_timeout]")
|| message.contains("[transaction_expired]")
{
Self::BadRequest(message)
} else if message.contains("[invalid_state") || message.contains("[savepoint") {
Self::Conflict(message)
} else {
Self::InternalError(message)
}
}
}
impl From<linkrs_api::api_core::CoreError> for HttpError {
fn from(err: linkrs_api::api_core::CoreError) -> Self {
use linkrs_api::api_core::CoreError;
match err {
CoreError::NotFound(msg) => HttpError::NotFound(msg),
CoreError::InvalidParameter(msg) => HttpError::BadRequest(msg),
CoreError::QueryExecutionFailed(msg) => HttpError::InternalError(msg),
CoreError::TransactionFailed(msg) => HttpError::transaction_message(msg),
CoreError::SchemaOperationFailed(msg) => HttpError::InternalError(msg),
CoreError::StorageError(msg) => HttpError::InternalError(msg),
CoreError::Internal(msg) => HttpError::InternalError(msg),
CoreError::DetailedQueryError { message, .. } => HttpError::InternalError(message),
CoreError::SyncError(msg) => HttpError::InternalError(msg),
CoreError::VectorError(msg) => HttpError::InternalError(msg),
}
}
}
#[cfg(test)]
mod tests {
use super::HttpError;
#[test]
fn transaction_error_codes_survive_handler_context() {
assert!(matches!(
HttpError::transaction_message("Failed to commit: [transaction_not_owner] denied"),
HttpError::Unauthorized(_)
));
assert!(matches!(
HttpError::transaction_message("Failed to commit: [write_transaction_conflict]"),
HttpError::Conflict(_)
));
assert!(matches!(
HttpError::transaction_message("Failed to commit: [transaction_timeout]"),
HttpError::BadRequest(_)
));
}
}