Skip to main content

envoy/
error.rs

1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum EnvoyError {
7    #[error("graph error: {0}")]
8    Graph(#[from] sqlitegraph::SqliteGraphError),
9
10    #[error("atheneum error: {0}")]
11    Atheneum(#[from] anyhow::Error),
12
13    #[error("serialization error: {0}")]
14    Serialization(#[from] serde_json::Error),
15
16    #[error("channel not found: {0}")]
17    ChannelNotFound(String),
18
19    #[error("channel already exists: {0}")]
20    ChannelAlreadyExists(String),
21
22    #[error("agent not subscribed to channel {channel}")]
23    NotSubscribed { agent: String, channel: String },
24
25    #[error("invalid entity: {0}")]
26    InvalidEntity(String),
27
28    #[error("agent not found: {0}")]
29    AgentNotFound(String),
30
31    #[error("agent offline: {0}")]
32    AgentOffline(String),
33
34    #[error("agent retired: {0}")]
35    AgentRetired(String),
36
37    #[error("agent already exists: {0}")]
38    AgentAlreadyExists(String),
39
40    #[error("message not found: {0}")]
41    MessageNotFound(String),
42
43    #[error("invalid message: {0}")]
44    InvalidMessage(String),
45
46    #[error("websocket error: {0}")]
47    WsError(String),
48
49    #[error("message too large: {0} bytes exceeds 1MB limit")]
50    MessageTooLarge(usize),
51
52    #[error("too many parts: {0} exceeds 20 limit")]
53    TooManyParts(usize),
54
55    #[error("database error: {0}")]
56    Database(#[from] rusqlite::Error),
57
58    #[error("agent status stale: {agent_id} (last heartbeat: {last_heartbeat:?}, threshold: {threshold_minutes}m)")]
59    StaleAgent {
60        agent_id: String,
61        last_heartbeat: Option<String>,
62        threshold_minutes: i64,
63    },
64
65    #[error("dependency already exists: {dependent} is already waiting on {blocker}")]
66    DuplicateDependency { dependent: String, blocker: String },
67
68    #[error("dependency not found: {0}")]
69    DependencyNotFound(String),
70
71    #[error("task not found: {0}")]
72    TaskNotFound(String),
73
74    #[error("task already claimed: {0} by {1}")]
75    TaskAlreadyClaimed(String, String),
76
77    #[error("invalid task state transition: {task_id} from {from} to {to}")]
78    InvalidTaskState {
79        task_id: String,
80        from: String,
81        to: String,
82    },
83
84    #[error("not task claimant: agent {agent} does not own task {task_id}")]
85    NotTaskClaimant { agent: String, task_id: String },
86
87    #[error("project config not found: {0}")]
88    ProjectConfigNotFound(String),
89
90    #[error("subscription not found: {0} for project {1}")]
91    SubscriptionNotFound(String, String),
92
93    #[error("lock poisoned: {0}")]
94    LockPoisoned(String),
95}
96
97impl IntoResponse for EnvoyError {
98    fn into_response(self) -> Response {
99        let (status, code) = match &self {
100            Self::AgentNotFound(_) => (StatusCode::NOT_FOUND, "AGENT_NOT_FOUND"),
101            Self::AgentOffline(_) => (StatusCode::CONFLICT, "AGENT_OFFLINE"),
102            Self::AgentRetired(_) => (StatusCode::GONE, "AGENT_RETIRED"),
103            Self::AgentAlreadyExists(_) => (StatusCode::CONFLICT, "AGENT_ALREADY_EXISTS"),
104            Self::MessageNotFound(_) => (StatusCode::NOT_FOUND, "MESSAGE_NOT_FOUND"),
105            Self::ChannelNotFound(_) => (StatusCode::NOT_FOUND, "CHANNEL_NOT_FOUND"),
106            Self::InvalidMessage(_) => (StatusCode::BAD_REQUEST, "INVALID_MESSAGE"),
107            Self::MessageTooLarge(_) => (StatusCode::BAD_REQUEST, "MESSAGE_TOO_LARGE"),
108            Self::TooManyParts(_) => (StatusCode::BAD_REQUEST, "TOO_MANY_PARTS"),
109            Self::Serialization(_) => (StatusCode::BAD_REQUEST, "SERIALIZATION_ERROR"),
110            Self::StaleAgent { .. } => (StatusCode::OK, "STALE_AGENT"),
111            Self::DuplicateDependency { .. } => (StatusCode::CONFLICT, "DUPLICATE_DEPENDENCY"),
112            Self::DependencyNotFound(_) => (StatusCode::NOT_FOUND, "DEPENDENCY_NOT_FOUND"),
113            Self::TaskNotFound(_) => (StatusCode::NOT_FOUND, "TASK_NOT_FOUND"),
114            Self::TaskAlreadyClaimed(_, _) => (StatusCode::CONFLICT, "TASK_ALREADY_CLAIMED"),
115            Self::InvalidTaskState { .. } => (StatusCode::CONFLICT, "INVALID_TASK_STATE"),
116            Self::NotTaskClaimant { .. } => (StatusCode::FORBIDDEN, "NOT_TASK_CLAIMANT"),
117            Self::ProjectConfigNotFound(_) => (StatusCode::NOT_FOUND, "PROJECT_CONFIG_NOT_FOUND"),
118            Self::SubscriptionNotFound(_, _) => (StatusCode::NOT_FOUND, "SUBSCRIPTION_NOT_FOUND"),
119            Self::LockPoisoned(_) => (StatusCode::INTERNAL_SERVER_ERROR, "LOCK_POISONED"),
120            _ => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR"),
121        };
122
123        let body = serde_json::json!({
124            "error": {
125                "code": code,
126                "message": self.to_string()
127            }
128        });
129
130        (status, axum::Json(body)).into_response()
131    }
132}
133
134pub type Result<T> = std::result::Result<T, EnvoyError>;