Skip to content
Clone
e7ef057
241 lines 8.2 KB Rust
use std::fmt;
use axum::Json;
use axum::extract::rejection::{JsonRejection, PathRejection, QueryRejection};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use export::rest::{AppResponseData, NoData};
use utoipa::IntoResponses;
#[derive(Debug)]
pub enum ChatError {
BadRequest(String),
Unauthorized,
Forbidden,
NotFound,
Conflict(String),
RateLimited(String),
Internal(String),
}
impl ChatError {
pub fn is_rate_limited(&self) -> bool {
matches!(self, Self::RateLimited(_))
}
}
impl fmt::Display for ChatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadRequest(message) => write!(f, "{message}"),
Self::Unauthorized => write!(f, "unauthorized"),
Self::Forbidden => write!(f, "forbidden"),
Self::NotFound => write!(f, "not found"),
Self::Conflict(message) => write!(f, "{message}"),
Self::RateLimited(message) => write!(f, "{message}"),
Self::Internal(message) => write!(f, "{message}"),
}
}
}
impl std::error::Error for ChatError {}
impl From<sea_orm::DbErr> for ChatError {
fn from(error: sea_orm::DbErr) -> Self {
if is_unique_violation(&error) {
return Self::Conflict("resource already exists".to_string());
}
Self::Internal(format!("database error: {error}"))
}
}
impl From<sea_orm::TransactionError<sea_orm::DbErr>> for ChatError {
fn from(error: sea_orm::TransactionError<sea_orm::DbErr>) -> Self {
match &error {
sea_orm::TransactionError::Transaction(db_err) if is_unique_violation(db_err) => {
Self::Conflict("resource already exists".to_string())
}
_ => Self::Internal(format!("database transaction error: {error}")),
}
}
}
impl From<sea_orm::TransactionError<ChatError>> for ChatError {
fn from(error: sea_orm::TransactionError<ChatError>) -> Self {
match error {
sea_orm::TransactionError::Connection(db_err) => Self::from(db_err),
sea_orm::TransactionError::Transaction(chat_err) => chat_err,
}
}
}
pub(crate) fn is_unique_violation(err: &sea_orm::DbErr) -> bool {
let msg = err.to_string().to_lowercase();
msg.contains("unique") || msg.contains("duplicate") || msg.contains("23505")
}
impl From<space::errors::SpaceError> for ChatError {
fn from(error: space::errors::SpaceError) -> Self {
match error {
space::errors::SpaceError::BadRequest(msg) => Self::BadRequest(msg),
space::errors::SpaceError::Unauthorized => Self::Unauthorized,
space::errors::SpaceError::Forbidden => Self::Forbidden,
space::errors::SpaceError::NotFound => Self::NotFound,
space::errors::SpaceError::Conflict(msg) => Self::Conflict(msg),
space::errors::SpaceError::RateLimited(msg) => Self::RateLimited(msg),
space::errors::SpaceError::Internal(msg) => Self::Internal(msg),
}
}
}
impl From<export::ExportError> for ChatError {
fn from(error: export::ExportError) -> Self {
Self::Internal(format!("infrastructure error: {error}"))
}
}
impl From<storage::StorageError> for ChatError {
fn from(error: storage::StorageError) -> Self {
Self::Internal(format!("storage error: {error}"))
}
}
impl From<redis::RedisError> for ChatError {
fn from(error: redis::RedisError) -> Self {
Self::Internal(format!("redis error: {error}"))
}
}
impl From<serde_json::Error> for ChatError {
fn from(error: serde_json::Error) -> Self {
Self::BadRequest(format!("invalid payload: {error}"))
}
}
#[derive(Debug, IntoResponses)]
pub enum ApiError {
#[response(status = 400, description = "request failed validation")]
BadRequest(#[to_schema] AppResponseData<NoData>),
#[response(status = 401, description = "authentication required")]
Unauthorized(#[to_schema] AppResponseData<NoData>),
#[response(status = 403, description = "operation not allowed")]
Forbidden(#[to_schema] AppResponseData<NoData>),
#[response(status = 404, description = "resource not found")]
NotFound(#[to_schema] AppResponseData<NoData>),
#[response(status = 409, description = "resource conflict")]
Conflict(#[to_schema] AppResponseData<NoData>),
#[response(status = 429, description = "rate limit exceeded")]
TooManyRequests(#[to_schema] AppResponseData<NoData>),
#[response(status = 500, description = "internal server error")]
Internal(#[to_schema] AppResponseData<NoData>),
}
impl ApiError {
fn payload(code: i32, message: &'static str) -> AppResponseData<NoData> {
AppResponseData::err(code, message)
}
fn decompose(self) -> (u16, AppResponseData<NoData>) {
match self {
Self::BadRequest(envelope) => (400, envelope),
Self::Unauthorized(envelope) => (401, envelope),
Self::Forbidden(envelope) => (403, envelope),
Self::NotFound(envelope) => (404, envelope),
Self::Conflict(envelope) => (409, envelope),
Self::TooManyRequests(envelope) => (429, envelope),
Self::Internal(envelope) => (500, envelope),
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (code, envelope) = self.decompose();
let status = match StatusCode::from_u16(code) {
Ok(status) => status,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(envelope)).into_response()
}
}
impl From<ChatError> for ApiError {
fn from(error: ChatError) -> Self {
match error {
ChatError::BadRequest(msg) => {
Self::BadRequest(AppResponseData::err(400, msg).with_error_code("bad_request"))
}
ChatError::Unauthorized => Self::Unauthorized(
Self::payload(401, "unauthorized").with_error_code("unauthorized"),
),
ChatError::Forbidden => {
Self::Forbidden(Self::payload(403, "forbidden").with_error_code("forbidden"))
}
ChatError::NotFound => Self::NotFound(
Self::payload(404, "chat resource not found").with_error_code("not_found"),
),
ChatError::Conflict(msg) => {
Self::Conflict(AppResponseData::err(409, msg).with_error_code("conflict"))
}
ChatError::RateLimited(msg) => Self::TooManyRequests(
AppResponseData::err(429, msg).with_error_code("rate_limited"),
),
ChatError::Internal(message) => {
tracing::warn!(error = %message, "chat handler failed");
Self::Internal(Self::payload(500, "internal server error"))
}
}
}
}
impl From<Box<ApiError>> for ApiError {
fn from(error: Box<ApiError>) -> Self {
*error
}
}
impl From<session::SessionError> for ApiError {
fn from(error: session::SessionError) -> Self {
match error {
session::SessionError::Unauthorized => {
Self::Unauthorized(Self::payload(401, "unauthorized"))
}
session::SessionError::Database(message) => {
tracing::warn!(error = %message, "chat handler failed");
Self::Internal(Self::payload(500, "internal server error"))
}
}
}
}
impl From<PathRejection> for ApiError {
fn from(error: PathRejection) -> Self {
Self::BadRequest(
AppResponseData::err(400, "invalid request path").with_error(error.body_text()),
)
}
}
impl From<JsonRejection> for ApiError {
fn from(error: JsonRejection) -> Self {
Self::BadRequest(
AppResponseData::err(400, "invalid request body")
.with_error(error.body_text())
.with_error_code("invalid_request_body"),
)
}
}
impl From<serde_json::Error> for ApiError {
fn from(_error: serde_json::Error) -> Self {
Self::BadRequest(Self::payload(400, "bad request"))
}
}
impl From<QueryRejection> for ApiError {
fn from(error: QueryRejection) -> Self {
Self::BadRequest(
AppResponseData::err(400, "invalid query parameters").with_error(error.body_text()),
)
}
}