use super::error_response::ErrorResponse;
use axum::Json;
use axum::extract::rejection::{JsonRejection, PathRejection, QueryRejection};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use stano_common::ServiceError;
use std::sync::Arc;
#[derive(Debug)]
pub enum ApiError {
JsonExtraction {
status: StatusCode,
body_text: String,
rejection_type: String,
},
PathExtraction {
status: StatusCode,
body_text: String,
rejection_type: String,
},
QueryExtraction {
status: StatusCode,
body_text: String,
rejection_type: String,
},
Service(ServiceError),
Internal(String),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, code, message, details, should_log) = match &self {
ApiError::JsonExtraction {
status, body_text, ..
} => (
*status,
"INVALID_JSON",
"Failed to parse JSON request body",
Some(body_text.clone()),
true, ),
ApiError::PathExtraction {
status, body_text, ..
} => (
*status,
"INVALID_PATH",
"Invalid path parameter",
Some(body_text.clone()),
true, ),
ApiError::QueryExtraction {
status, body_text, ..
} => (
*status,
"INVALID_QUERY",
"Invalid query parameter",
Some(body_text.clone()),
true, ),
ApiError::Service(service_err) => {
match service_err {
ServiceError::NotFound => (
StatusCode::NOT_FOUND,
"NOT_FOUND",
"Resource not found",
None,
false, ),
ServiceError::InvalidInput(msg) => (
StatusCode::BAD_REQUEST,
"INVALID_INPUT",
"Invalid request",
Some(msg.clone()),
true,
),
ServiceError::Conflict(msg) => (
StatusCode::CONFLICT,
"CONFLICT",
"Resource conflict",
Some(msg.clone()),
true,
),
ServiceError::Unauthorized => (
StatusCode::UNAUTHORIZED,
"UNAUTHORIZED",
"Authentication required",
None,
false, ),
ServiceError::Forbidden => (
StatusCode::FORBIDDEN,
"FORBIDDEN",
"Insufficient permissions",
None,
false,
),
ServiceError::Internal(_err) => (
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"An internal error occurred",
None, true,
),
}
}
ApiError::Internal(_msg) => (
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"An internal error occurred",
None, true,
),
};
if should_log {
tracing::error!(
error_type = ?self,
status = %status,
code = %code,
"API error occurred"
);
}
let mut error_response = ErrorResponse::new(status.as_u16(), code, message);
if status.is_client_error()
&& let Some(d) = details
{
error_response = error_response.with_details(d);
}
let mut response = (status, Json(error_response)).into_response();
response.extensions_mut().insert(Arc::new(self));
response
}
}
impl From<JsonRejection> for ApiError {
fn from(rejection: JsonRejection) -> Self {
tracing::warn!(
rejection_status = %rejection.status(),
rejection_body = %rejection.body_text(),
"JSON extraction failed"
);
ApiError::JsonExtraction {
status: rejection.status(),
body_text: rejection.body_text(),
rejection_type: std::any::type_name_of_val(&rejection).to_string(),
}
}
}
impl From<PathRejection> for ApiError {
fn from(rejection: PathRejection) -> Self {
tracing::warn!(
rejection_status = %rejection.status(),
rejection_body = %rejection.body_text(),
"Path extraction failed"
);
ApiError::PathExtraction {
status: rejection.status(),
body_text: rejection.body_text(),
rejection_type: std::any::type_name_of_val(&rejection).to_string(),
}
}
}
impl From<QueryRejection> for ApiError {
fn from(rejection: QueryRejection) -> Self {
tracing::warn!(
rejection_status = %rejection.status(),
rejection_body = %rejection.body_text(),
"Query extraction failed"
);
ApiError::QueryExtraction {
status: rejection.status(),
body_text: rejection.body_text(),
rejection_type: std::any::type_name_of_val(&rejection).to_string(),
}
}
}
impl From<ServiceError> for ApiError {
fn from(error: ServiceError) -> Self {
ApiError::Service(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn service_error_not_found_maps_correctly() {
let api_err = ApiError::from(ServiceError::NotFound);
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn service_error_invalid_input_maps_correctly() {
let api_err = ApiError::from(ServiceError::InvalidInput("test".to_string()));
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn service_error_conflict_maps_correctly() {
let api_err = ApiError::from(ServiceError::Conflict("duplicate entry".to_string()));
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::CONFLICT);
}
#[test]
fn service_error_unauthorized_maps_correctly() {
let api_err = ApiError::from(ServiceError::Unauthorized);
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn service_error_forbidden_maps_correctly() {
let api_err = ApiError::from(ServiceError::Forbidden);
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn service_error_internal_maps_correctly() {
let api_err = ApiError::from(ServiceError::Internal(anyhow::anyhow!("database error")));
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn internal_error_hides_details() {
let api_err = ApiError::Internal("sensitive info".to_string());
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn json_extraction_error_returns_bad_request() {
let api_err = ApiError::JsonExtraction {
status: StatusCode::BAD_REQUEST,
body_text: "invalid json".to_string(),
rejection_type: "JsonRejection".to_string(),
};
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn path_extraction_error_returns_bad_request() {
let api_err = ApiError::PathExtraction {
status: StatusCode::BAD_REQUEST,
body_text: "invalid path".to_string(),
rejection_type: "PathRejection".to_string(),
};
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn query_extraction_error_returns_bad_request() {
let api_err = ApiError::QueryExtraction {
status: StatusCode::BAD_REQUEST,
body_text: "invalid query".to_string(),
rejection_type: "QueryRejection".to_string(),
};
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn api_error_stored_in_response_extensions() {
let api_err = ApiError::from(ServiceError::NotFound);
let response = api_err.into_response();
let has_error = response.extensions().get::<Arc<ApiError>>().is_some();
assert!(
has_error,
"ApiError should be stored in response extensions"
);
}
#[test]
fn client_errors_include_details() {
let api_err = ApiError::from(ServiceError::InvalidInput("email is required".to_string()));
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
assert!(response.status().is_client_error());
}
#[test]
fn server_errors_hide_details() {
let api_err = ApiError::Internal("database connection failed".to_string());
let response = api_err.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(response.status().is_server_error());
}
}