use axum::Json;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use slave_framework::turso;
/// Errors returned by request handlers, rendered as JSON responses.
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("{0} not found")]
NotFound(String),
#[error("{0}")]
Validation(String),
#[error("{0}")]
Conflict(String),
#[error("authentication required")]
Unauthorized,
#[error("invalid email or password")]
InvalidCredentials,
#[error("you do not have permission to do this")]
Forbidden,
#[error("database error: {0}")]
Database(#[from] turso::Error),
#[error("{0}")]
Internal(String),
}
pub type AppResult<T> = Result<T, AppError>;
impl From<slave_framework::Error> for AppError {
fn from(err: slave_framework::Error) -> Self {
Self::Internal(err.to_string())
}
}
#[derive(Serialize)]
struct ErrorBody {
error: String,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = match &self {
Self::NotFound(_) => StatusCode::NOT_FOUND,
Self::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
Self::Conflict(_) => StatusCode::CONFLICT,
Self::Unauthorized | Self::InvalidCredentials => StatusCode::UNAUTHORIZED,
Self::Forbidden => StatusCode::FORBIDDEN,
Self::Database(_) | Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
// Server-side failures are logged in full but never leaked to clients.
let message = if status == StatusCode::INTERNAL_SERVER_ERROR {
if self.to_string().contains("no such table") {
tracing::error!("{self} (have you run `slave migrate`?)");
} else {
tracing::error!("{self}");
}
"internal server error".to_owned()
} else {
self.to_string()
};
let mut response = (status, Json(ErrorBody { error: message })).into_response();
if matches!(self, Self::Unauthorized) {
response.headers_mut().insert(
header::WWW_AUTHENTICATE,
header::HeaderValue::from_static("Bearer"),
);
}
response
}
}