use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use nemo_relay::error::FlowError;
use serde::Serialize;
use serde_json::{Map, Value, json};
use strum::Display;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub(crate) enum PluginLifecycleFailureKind {
Failed,
NotFound,
Refused,
}
pub(crate) type PluginLifecycleErrorContext<'a> = (
&'static str,
Option<&'a str>,
PluginLifecycleFailureKind,
Option<&'static str>,
&'a str,
);
#[derive(Debug, thiserror::Error)]
pub(crate) enum CliError {
#[error("guardrail rejected: {0}")]
GuardrailRejected(String),
#[error("invalid hook payload: {0}")]
InvalidPayload(String),
#[error("payload too large: {0}")]
PayloadTooLarge(String),
#[error("gateway upstream error: {0}")]
Upstream(#[from] reqwest::Error),
#[error("http error: {0}")]
Http(#[from] http::Error),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("installer error: {0}")]
Install(String),
#[error("configuration error: {0}")]
Config(String),
#[error("launcher error: {0}")]
Launch(String),
#[error("{message}")]
PluginLifecycle {
command: &'static str,
target: Option<String>,
kind: PluginLifecycleFailureKind,
code: Option<&'static str>,
message: String,
},
#[error("NeMo Relay runtime error: {0}")]
Flow(#[from] nemo_relay::error::FlowError),
#[error("openinference error: {0}")]
OpenInference(#[from] nemo_relay::observability::openinference::OpenInferenceError),
}
impl CliError {
pub(crate) fn guardrail_rejection_reason(&self) -> Option<&str> {
match self {
Self::GuardrailRejected(reason) => Some(reason),
Self::Flow(FlowError::GuardrailRejected(reason)) => Some(reason),
_ => None,
}
}
pub(crate) fn as_plugin_lifecycle_error_context(
&self,
) -> Option<PluginLifecycleErrorContext<'_>> {
match self {
Self::PluginLifecycle {
command,
target,
kind,
code,
message,
} => Some((command, target.as_deref(), *kind, *code, message.as_str())),
_ => None,
}
}
}
impl IntoResponse for CliError {
fn into_response(self) -> Response {
let message = self.to_string();
let guardrail_reason = self.guardrail_rejection_reason().map(ToOwned::to_owned);
let status = match (guardrail_reason.is_some(), self) {
(true, _) => StatusCode::FORBIDDEN,
(false, Self::PayloadTooLarge(_)) => StatusCode::PAYLOAD_TOO_LARGE,
(false, Self::InvalidPayload(_)) => StatusCode::BAD_REQUEST,
(false, Self::Upstream(_)) => StatusCode::BAD_GATEWAY,
(
false,
Self::Http(_)
| Self::Io(_)
| Self::Install(_)
| Self::Config(_)
| Self::Launch(_)
| Self::Flow(_)
| Self::OpenInference(_),
) => StatusCode::INTERNAL_SERVER_ERROR,
(false, _) => StatusCode::INTERNAL_SERVER_ERROR,
};
let error_type = if guardrail_reason.is_some() {
"nemo_relay_guardrail_rejected"
} else {
"nemo_relay_gateway_error"
};
let mut error = Map::from_iter([
("message".to_string(), json!(message)),
("type".to_string(), json!(error_type)),
]);
if let Some(reason) = guardrail_reason {
error.insert("reason".to_string(), json!(reason));
}
let body = Json(json!({
"error": Value::Object(error)
}));
(status, body).into_response()
}
}