use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
#[derive(Debug, Clone)]
pub struct ApiError {
pub status: StatusCode,
pub message: String,
pub kind: &'static str,
pub param: Option<String>,
pub code: Option<String>,
}
impl ApiError {
pub fn invalid_request(message: impl Into<String>) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
message: message.into(),
kind: "invalid_request_error",
param: None,
code: None,
}
}
pub fn not_found(message: impl Into<String>) -> Self {
Self {
status: StatusCode::NOT_FOUND,
message: message.into(),
kind: "not_found_error",
param: None,
code: None,
}
}
pub fn unavailable(message: impl Into<String>) -> Self {
Self {
status: StatusCode::SERVICE_UNAVAILABLE,
message: message.into(),
kind: "service_unavailable",
param: None,
code: None,
}
}
pub fn internal(message: impl Into<String>) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: message.into(),
kind: "internal_error",
param: None,
code: None,
}
}
pub fn with_param(mut self, param: impl Into<String>) -> Self {
self.param = Some(param.into());
self
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub fn envelope(&self) -> serde_json::Value {
serde_json::json!({
"error": {
"message": self.message,
"type": self.kind,
"param": self.param,
"code": self.code,
}
})
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(self.status, Json(self.envelope())).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_render_openai_error_envelope_with_all_fields() {
let e = ApiError::invalid_request("temperature must be in [0, 2]")
.with_param("temperature")
.with_code("out_of_range");
let v = e.envelope();
assert_eq!(v["error"]["message"], "temperature must be in [0, 2]");
assert_eq!(v["error"]["type"], "invalid_request_error");
assert_eq!(v["error"]["param"], "temperature");
assert_eq!(v["error"]["code"], "out_of_range");
assert_eq!(e.status, StatusCode::BAD_REQUEST);
}
#[test]
fn should_null_absent_param_and_code() {
let v = ApiError::not_found("model `foo` not found").envelope();
assert!(v["error"]["param"].is_null());
assert!(v["error"]["code"].is_null());
assert_eq!(v["error"]["type"], "not_found_error");
}
#[test]
fn should_map_intents_to_statuses() {
assert_eq!(ApiError::not_found("x").status, StatusCode::NOT_FOUND);
assert_eq!(
ApiError::unavailable("x").status,
StatusCode::SERVICE_UNAVAILABLE
);
assert_eq!(
ApiError::internal("x").status,
StatusCode::INTERNAL_SERVER_ERROR
);
}
}