1use axum::{
2 http::StatusCode,
3 response::{IntoResponse, Response},
4 Json,
5};
6use serde::Serialize;
7use serde_json::json;
8use thiserror::Error;
9
10use crate::types::{ErrorCode, RuntimeErrorInfo};
11
12pub type AppResult<T> = Result<T, AppError>;
13
14#[derive(Debug, Error)]
15pub enum AppError {
16 #[error("invalid input: {0}")]
17 InvalidInput(String),
18 #[error("task not found: {0}")]
19 NotFound(String),
20 #[error("queue is full")]
21 QueueFull,
22 #[error("task is already terminal: {0}")]
23 Conflict(String),
24 #[error("launch failed: {0}")]
25 LaunchFailed(String),
26 #[error("sandbox setup failed: {0}")]
27 SandboxSetup(String),
28 #[error("unsupported capability: {0}")]
29 UnsupportedCapability(String),
30 #[error("insufficient resources: {0}")]
31 InsufficientResources(String),
32 #[error("io error: {0}")]
33 Io(#[from] std::io::Error),
34 #[error("sqlite error: {0}")]
35 Sqlite(#[from] rusqlite::Error),
36 #[error("json error: {0}")]
37 Json(#[from] serde_json::Error),
38 #[error("http error: {0}")]
39 Http(#[from] reqwest::Error),
40 #[error("internal error: {0}")]
41 Internal(String),
42}
43
44impl AppError {
45 pub fn code(&self) -> ErrorCode {
46 match self {
47 AppError::InvalidInput(_) => ErrorCode::InvalidInput,
48 AppError::NotFound(_) => ErrorCode::Internal,
49 AppError::QueueFull => ErrorCode::ResourceLimitExceeded,
50 AppError::Conflict(_) => ErrorCode::Internal,
51 AppError::LaunchFailed(_) => ErrorCode::LaunchFailed,
52 AppError::SandboxSetup(_) => ErrorCode::SandboxSetupFailed,
53 AppError::UnsupportedCapability(_) => ErrorCode::UnsupportedCapability,
54 AppError::InsufficientResources(_) => ErrorCode::InsufficientResources,
55 AppError::Io(_) | AppError::Sqlite(_) | AppError::Json(_) | AppError::Http(_) => {
56 ErrorCode::Internal
57 }
58 AppError::Internal(_) => ErrorCode::Internal,
59 }
60 }
61
62 pub fn status_code(&self) -> StatusCode {
63 match self {
64 AppError::InvalidInput(_) => StatusCode::BAD_REQUEST,
65 AppError::NotFound(_) => StatusCode::NOT_FOUND,
66 AppError::QueueFull => StatusCode::TOO_MANY_REQUESTS,
67 AppError::Conflict(_) => StatusCode::CONFLICT,
68 AppError::LaunchFailed(_)
69 | AppError::SandboxSetup(_)
70 | AppError::UnsupportedCapability(_)
71 | AppError::InsufficientResources(_) => StatusCode::BAD_REQUEST,
72 AppError::Io(_)
73 | AppError::Sqlite(_)
74 | AppError::Json(_)
75 | AppError::Http(_)
76 | AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
77 }
78 }
79
80 pub fn as_runtime_error(&self) -> RuntimeErrorInfo {
81 RuntimeErrorInfo {
82 code: self.code(),
83 message: self.to_string(),
84 details: None,
85 }
86 }
87}
88
89#[derive(Debug, Serialize)]
90struct ErrorEnvelope {
91 error: RuntimeErrorInfo,
92}
93
94impl IntoResponse for AppError {
95 fn into_response(self) -> Response {
96 let status = self.status_code();
97 let body = Json(ErrorEnvelope {
98 error: self.as_runtime_error(),
99 });
100 (status, body).into_response()
101 }
102}
103
104pub fn json_error(code: ErrorCode, message: impl Into<String>) -> serde_json::Value {
105 json!({
106 "error": {
107 "code": code,
108 "message": message.into(),
109 }
110 })
111}