use crate::orm::DynError;
use crate::orm::write::WriteError;
use crate::web::{IntoResponse, Json, Response, StatusCode};
#[derive(Debug)]
pub enum ApiError {
NotFound(String),
BadRequest(String),
Unauthorized(String),
Forbidden(String),
TooManyRequests(String),
Validation(WriteError),
Database(sqlx::Error),
Internal(String),
}
impl ApiError {
pub fn not_found(msg: impl Into<String>) -> Self {
Self::NotFound(msg.into())
}
pub fn unauthorized(msg: impl Into<String>) -> Self {
Self::Unauthorized(msg.into())
}
pub fn forbidden(msg: impl Into<String>) -> Self {
Self::Forbidden(msg.into())
}
pub fn too_many_requests(msg: impl Into<String>) -> Self {
Self::TooManyRequests(msg.into())
}
pub fn bad_request(msg: impl Into<String>) -> Self {
Self::BadRequest(msg.into())
}
pub fn internal(msg: impl Into<String>) -> Self {
Self::Internal(msg.into())
}
}
impl From<sqlx::Error> for ApiError {
fn from(e: sqlx::Error) -> Self {
Self::Database(e)
}
}
impl From<crate::templates::TemplateError> for ApiError {
fn from(e: crate::templates::TemplateError) -> Self {
Self::Internal(e.to_string())
}
}
impl From<WriteError> for ApiError {
fn from(e: WriteError) -> Self {
if e.is_validation() {
return Self::Validation(e);
}
match e {
WriteError::Sqlx(s) => Self::Database(s),
other => Self::Internal(other.to_string()),
}
}
}
impl From<DynError> for ApiError {
fn from(e: DynError) -> Self {
match e {
DynError::Write(w) => Self::from(w),
DynError::Sqlx(s) => Self::from(s),
}
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiError::NotFound(m)
| ApiError::BadRequest(m)
| ApiError::Unauthorized(m)
| ApiError::Forbidden(m)
| ApiError::TooManyRequests(m)
| ApiError::Internal(m) => {
write!(f, "{m}")
}
ApiError::Validation(e) => write!(f, "{e}"),
ApiError::Database(e) => write!(f, "database error: {e}"),
}
}
}
impl std::error::Error for ApiError {}
fn json_error(status: StatusCode, code: &str, error: &str) -> Response {
(
status,
Json(serde_json::json!({ "error": error, "code": code })),
)
.into_response()
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
match self {
ApiError::NotFound(msg) => json_error(StatusCode::NOT_FOUND, "not_found", &msg),
ApiError::BadRequest(msg) => json_error(StatusCode::BAD_REQUEST, "bad_request", &msg),
ApiError::Unauthorized(msg) => {
json_error(StatusCode::UNAUTHORIZED, "unauthorized", &msg)
}
ApiError::Forbidden(msg) => json_error(StatusCode::FORBIDDEN, "forbidden", &msg),
ApiError::TooManyRequests(msg) => {
json_error(StatusCode::TOO_MANY_REQUESTS, "too_many_requests", &msg)
}
ApiError::Validation(e) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"code": e.code(),
"field_errors": e.field_errors(),
"non_field_errors": e.non_field_errors(),
})),
)
.into_response(),
ApiError::Database(e) => {
tracing::error!(error = %e, "ApiError: database error");
json_error(
StatusCode::INTERNAL_SERVER_ERROR,
"database_error",
"internal server error",
)
}
ApiError::Internal(msg) => {
tracing::error!(detail = %msg, "ApiError: internal error");
json_error(
StatusCode::INTERNAL_SERVER_ERROR,
"internal_error",
"internal server error",
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::web::{IntoResponse, StatusCode};
#[test]
fn write_error_validation_becomes_a_400_with_the_field() {
let e = WriteError::RequiredFieldMissing {
field: "email".into(),
};
let api = ApiError::from(e);
assert_eq!(api.into_response().status(), StatusCode::BAD_REQUEST);
}
#[test]
fn a_bare_sqlx_error_becomes_an_opaque_500() {
let api = ApiError::from(sqlx::Error::RowNotFound);
assert_eq!(
api.into_response().status(),
StatusCode::INTERNAL_SERVER_ERROR
);
}
#[test]
fn dynerror_routes_write_to_400_and_sqlx_to_500() {
let v = ApiError::from(DynError::Write(WriteError::RequiredFieldMissing {
field: "x".into(),
}));
assert_eq!(v.into_response().status(), StatusCode::BAD_REQUEST);
let s = ApiError::from(DynError::Sqlx(sqlx::Error::RowNotFound));
assert_eq!(
s.into_response().status(),
StatusCode::INTERNAL_SERVER_ERROR
);
}
#[test]
fn explicit_constructors_carry_their_status() {
assert_eq!(
ApiError::not_found("nope").into_response().status(),
StatusCode::NOT_FOUND
);
assert_eq!(
ApiError::bad_request("bad").into_response().status(),
StatusCode::BAD_REQUEST
);
assert_eq!(
ApiError::internal("boom").into_response().status(),
StatusCode::INTERNAL_SERVER_ERROR
);
}
}