Files
RustBox/src/error.rs
T
Mateusz Gruszczyński a643e7e3ab
RustBox CI / rustbox (push) Canceled after 0s
v0.0.5
2026-10-30 12:06:44 +01:00

180 lines
5.9 KiB
Rust

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Serialize;
#[derive(Debug, Serialize)]
struct ErrorEnvelope {
error: ErrorBody,
}
#[derive(Debug, Serialize)]
struct ErrorBody {
code: String,
message: String,
status: u16,
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum AppError {
InternalError(String),
NotFound(String),
Unauthorized(String),
BadRequest(String),
Forbidden(String),
ServiceUnavailable(String),
PayloadTooLarge(String),
}
impl AppError {
fn status_code(&self) -> StatusCode {
match self {
AppError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::NotFound(_) => StatusCode::NOT_FOUND,
AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
AppError::Forbidden(_) => StatusCode::FORBIDDEN,
AppError::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
AppError::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
}
}
fn error_code(&self) -> &'static str {
match self {
AppError::InternalError(_) => "INTERNAL_ERROR",
AppError::NotFound(_) => "NOT_FOUND",
AppError::Unauthorized(_) => "UNAUTHORIZED",
AppError::BadRequest(_) => "BAD_REQUEST",
AppError::Forbidden(_) => "FORBIDDEN",
AppError::ServiceUnavailable(_) => "SERVICE_UNAVAILABLE",
AppError::PayloadTooLarge(_) => "PAYLOAD_TOO_LARGE",
}
}
fn message(&self) -> &str {
match self {
AppError::InternalError(msg)
| AppError::NotFound(msg)
| AppError::Unauthorized(msg)
| AppError::BadRequest(msg)
| AppError::Forbidden(msg)
| AppError::ServiceUnavailable(msg)
| AppError::PayloadTooLarge(msg) => msg,
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = self.status_code();
let envelope = ErrorEnvelope {
error: ErrorBody {
code: self.error_code().to_string(),
message: self.message().to_string(),
status: status.as_u16(),
},
};
let body = serde_json::to_string(&envelope).unwrap_or_else(|_| {
r#"{"error":{"code":"INTERNAL_ERROR","message":"Failed to serialize error","status":500}}"#
.to_string()
});
(status, [("content-type", "application/json")], body).into_response()
}
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.error_code(), self.message())
}
}
#[cfg(test)]
mod tests {
use super::*;
use http_body_util::BodyExt;
async fn error_to_json(error: AppError) -> (StatusCode, serde_json::Value) {
let response = error.into_response();
let status = response.status();
let body = response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
let json: serde_json::Value =
serde_json::from_slice(&body).expect("body should be valid JSON");
(status, json)
}
#[tokio::test]
async fn internal_error_returns_500() {
let (status, json) = error_to_json(AppError::InternalError("something broke".into())).await;
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(json["error"]["code"], "INTERNAL_ERROR");
assert_eq!(json["error"]["message"], "something broke");
assert_eq!(json["error"]["status"], 500);
}
#[tokio::test]
async fn not_found_returns_404() {
let (status, json) = error_to_json(AppError::NotFound("mailbox missing".into())).await;
assert_eq!(status, StatusCode::NOT_FOUND);
assert_eq!(json["error"]["code"], "NOT_FOUND");
assert_eq!(json["error"]["message"], "mailbox missing");
assert_eq!(json["error"]["status"], 404);
}
#[tokio::test]
async fn unauthorized_returns_401() {
let (status, json) = error_to_json(AppError::Unauthorized("bad token".into())).await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
assert_eq!(json["error"]["code"], "UNAUTHORIZED");
assert_eq!(json["error"]["message"], "bad token");
assert_eq!(json["error"]["status"], 401);
}
#[tokio::test]
async fn bad_request_returns_400() {
let (status, json) = error_to_json(AppError::BadRequest("invalid input".into())).await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(json["error"]["code"], "BAD_REQUEST");
assert_eq!(json["error"]["message"], "invalid input");
assert_eq!(json["error"]["status"], 400);
}
#[tokio::test]
async fn forbidden_returns_403() {
let (status, json) = error_to_json(AppError::Forbidden("blocked".into())).await;
assert_eq!(status, StatusCode::FORBIDDEN);
assert_eq!(json["error"]["code"], "FORBIDDEN");
assert_eq!(json["error"]["status"], 403);
}
#[tokio::test]
async fn payload_too_large_returns_413() {
let (status, json) =
error_to_json(AppError::PayloadTooLarge("message too large".into())).await;
assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
assert_eq!(json["error"]["code"], "PAYLOAD_TOO_LARGE");
assert_eq!(json["error"]["status"], 413);
}
#[tokio::test]
async fn service_unavailable_returns_503() {
let (status, json) = error_to_json(AppError::ServiceUnavailable("IMAP down".into())).await;
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(json["error"]["code"], "SERVICE_UNAVAILABLE");
assert_eq!(json["error"]["message"], "IMAP down");
assert_eq!(json["error"]["status"], 503);
}
}