worldinterface_daemon/
error.rs1use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use axum::Json;
6use serde::Serialize;
7use worldinterface_host::HostError;
8use worldinterface_http_trigger::WebhookError;
9
10#[derive(Debug, thiserror::Error)]
12pub enum DaemonError {
13 #[error("host error: {0}")]
15 Host(#[from] HostError),
16
17 #[error("configuration error: {0}")]
19 Config(String),
20
21 #[error("failed to bind: {0}")]
23 Bind(#[source] std::io::Error),
24
25 #[error("server error: {0}")]
27 Serve(#[source] std::io::Error),
28
29 #[error("I/O error: {0}")]
31 Io(#[from] std::io::Error),
32
33 #[error("config parse error: {0}")]
35 ConfigParse(#[from] toml::de::Error),
36}
37
38#[derive(Debug, thiserror::Error)]
40pub enum ApiError {
41 #[error("{0}")]
43 NotFound(String),
44
45 #[error("{0}")]
47 BadRequest(String),
48
49 #[error("{0}")]
51 Conflict(String),
52
53 #[error("{0}")]
55 Internal(String),
56}
57
58#[derive(Serialize)]
59struct ErrorResponse {
60 error: String,
61}
62
63impl IntoResponse for ApiError {
64 fn into_response(self) -> Response {
65 let (status, message) = match &self {
66 ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
67 ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
68 ApiError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()),
69 ApiError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
70 };
71 (status, Json(ErrorResponse { error: message })).into_response()
72 }
73}
74
75impl From<HostError> for ApiError {
76 fn from(err: HostError) -> Self {
77 match err {
78 HostError::FlowRunNotFound(_) => ApiError::NotFound(err.to_string()),
79 HostError::ConnectorNotFound(_) => ApiError::NotFound(err.to_string()),
80 HostError::InvalidConfig(_) => ApiError::BadRequest(err.to_string()),
81 HostError::Compilation(_) => ApiError::BadRequest(err.to_string()),
82 HostError::FlowFailed { .. } => ApiError::Internal(err.to_string()),
83 HostError::FlowCanceled(_) => ApiError::Internal(err.to_string()),
84 _ => ApiError::Internal(err.to_string()),
85 }
86 }
87}
88
89impl From<WebhookError> for ApiError {
90 fn from(err: WebhookError) -> Self {
91 match &err {
92 WebhookError::PathAlreadyRegistered(_) => ApiError::Conflict(err.to_string()),
93 WebhookError::WebhookNotFound(_) => ApiError::NotFound(err.to_string()),
94 WebhookError::PathNotFound(_) => ApiError::NotFound(err.to_string()),
95 WebhookError::InvalidPath(_) => ApiError::BadRequest(err.to_string()),
96 _ => ApiError::Internal(err.to_string()),
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use axum::response::IntoResponse;
104 use worldinterface_core::id::FlowRunId;
105
106 use super::*;
107
108 #[test]
109 fn host_not_found_maps_to_404() {
110 let err: ApiError = HostError::FlowRunNotFound(FlowRunId::new()).into();
111 assert!(matches!(err, ApiError::NotFound(_)));
112 }
113
114 #[test]
115 fn host_connector_not_found_maps_to_404() {
116 let err: ApiError = HostError::ConnectorNotFound("foo".to_string()).into();
117 assert!(matches!(err, ApiError::NotFound(_)));
118 }
119
120 #[test]
121 fn host_invalid_config_maps_to_400() {
122 let err: ApiError = HostError::InvalidConfig("bad".to_string()).into();
123 assert!(matches!(err, ApiError::BadRequest(_)));
124 }
125
126 #[test]
127 fn host_internal_error_maps_to_500() {
128 let err: ApiError = HostError::InternalError("bug".to_string()).into();
129 assert!(matches!(err, ApiError::Internal(_)));
130 }
131
132 #[test]
133 fn api_error_serializes_to_json() {
134 let err = ApiError::NotFound("gone".to_string());
135 let response = err.into_response();
136 assert_eq!(response.status(), StatusCode::NOT_FOUND);
137 }
138
139 #[test]
140 fn api_error_bad_request_status() {
141 let err = ApiError::BadRequest("invalid".to_string());
142 let response = err.into_response();
143 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
144 }
145
146 #[test]
147 fn api_error_internal_status() {
148 let err = ApiError::Internal("crash".to_string());
149 let response = err.into_response();
150 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
151 }
152
153 #[test]
154 fn api_error_conflict_status() {
155 let err = ApiError::Conflict("duplicate".to_string());
156 let response = err.into_response();
157 assert_eq!(response.status(), StatusCode::CONFLICT);
158 }
159
160 #[test]
163 fn webhook_path_registered_maps_to_409() {
164 let err: ApiError = WebhookError::PathAlreadyRegistered("github/push".to_string()).into();
165 assert!(matches!(err, ApiError::Conflict(_)));
166 let response = err.into_response();
167 assert_eq!(response.status(), StatusCode::CONFLICT);
168 }
169
170 #[test]
171 fn webhook_not_found_maps_to_404() {
172 let err: ApiError = WebhookError::WebhookNotFound(worldinterface_http_trigger::WebhookId::new()).into();
173 assert!(matches!(err, ApiError::NotFound(_)));
174 }
175
176 #[test]
177 fn webhook_path_not_found_maps_to_404() {
178 let err: ApiError = WebhookError::PathNotFound("unknown".to_string()).into();
179 assert!(matches!(err, ApiError::NotFound(_)));
180 }
181
182 #[test]
183 fn webhook_invalid_path_maps_to_400() {
184 let err: ApiError = WebhookError::InvalidPath("bad".to_string()).into();
185 assert!(matches!(err, ApiError::BadRequest(_)));
186 }
187}