use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub type Result<T, E = ApiError> = core::result::Result<T, E>;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct FaultMessage {
pub fault_message: String,
}
impl FaultMessage {
pub fn new(reason: impl Into<String>) -> Self {
Self {
fault_message: reason.into(),
}
}
}
#[derive(Debug, Error)]
pub enum ApiError {
#[error("{0}")]
BadRequest(String),
#[error("{0}")]
PayloadTooLarge(String),
#[error("Resource not found: {0}")]
NotFound(String),
#[error("VMM action timed out: {0}")]
Timeout(&'static str),
#[error("internal error: {0}")]
Internal(String),
}
impl ApiError {
pub fn status(&self) -> StatusCode {
match self {
Self::BadRequest(_) | Self::NotFound(_) => StatusCode::BAD_REQUEST,
Self::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
Self::Timeout(_) => StatusCode::GATEWAY_TIMEOUT,
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub fn fault_message(&self) -> String {
match self {
Self::BadRequest(s) | Self::PayloadTooLarge(s) | Self::Internal(s) => s.clone(),
Self::NotFound(path) => format!("No such resource: {path}"),
Self::Timeout(class) => format!("VMM action timed out: {class}"),
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let status = self.status();
let body = FaultMessage::new(self.fault_message());
(status, Json(body)).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fault_message_serializes_with_correct_field_name() {
let msg = FaultMessage::new("oops");
let json = serde_json::to_string(&msg).unwrap();
assert_eq!(json, r#"{"fault_message":"oops"}"#);
}
#[test]
fn fault_message_round_trips_through_serde() {
let original = FaultMessage::new("kernel image not found");
let json = serde_json::to_string(&original).unwrap();
let back: FaultMessage = serde_json::from_str(&json).unwrap();
assert_eq!(original, back);
}
#[test]
fn bad_request_maps_to_400() {
let err = ApiError::BadRequest("bad".into());
assert_eq!(err.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn payload_too_large_maps_to_413() {
let err = ApiError::PayloadTooLarge("too big".into());
assert_eq!(err.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[test]
fn not_found_maps_to_400_for_firecracker_parity() {
let err = ApiError::NotFound("/missing".into());
assert_eq!(err.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn timeout_maps_to_504() {
let err = ApiError::Timeout("PUT /actions {InstanceStart}");
assert_eq!(err.status(), StatusCode::GATEWAY_TIMEOUT);
assert!(err.fault_message().contains("InstanceStart"));
}
#[test]
fn internal_maps_to_500() {
let err = ApiError::Internal("event loop gone".into());
assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn not_found_fault_message_contains_path() {
let err = ApiError::NotFound("/no-such".into());
assert_eq!(err.fault_message(), "No such resource: /no-such");
}
}