1use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use serde::{Deserialize, Serialize};
8use sokuto_core::error::CoreError;
9use sokuto_runtime::error::RuntimeError;
10use utoipa::ToSchema;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
14pub struct ErrorDetail {
15 pub message: String,
17 #[serde(rename = "type")]
19 pub error_type: String,
20 pub code: String,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
26pub struct ErrorResponse {
27 pub error: ErrorDetail,
29}
30
31impl ErrorResponse {
32 pub fn new(
34 message: impl Into<String>,
35 error_type: impl Into<String>,
36 code: impl Into<String>,
37 ) -> Self {
38 Self {
39 error: ErrorDetail {
40 message: message.into(),
41 error_type: error_type.into(),
42 code: code.into(),
43 },
44 }
45 }
46}
47
48#[derive(Debug, thiserror::Error)]
50pub enum ServerError {
51 #[error("不正なリクエスト: {0}")]
53 BadRequest(String),
54
55 #[error("スキーマ検証エラー: {0}")]
57 ValidationError(#[from] CoreError),
58
59 #[error("リソース上限超過: {0}")]
61 PayloadTooLarge(String),
62
63 #[error("推論エンジン障害: {0}")]
65 Runtime(#[from] RuntimeError),
66
67 #[error("DAG 実行エラー: {0}")]
69 Dag(#[from] sokuto_runtime::dag::DagError),
70
71 #[error("ガードレール違反: {0}")]
73 GuardrailViolation(String),
74
75 #[error("State キャッシュミス: {0}")]
77 StateCacheMiss(String),
78
79 #[error("サーバー内部エラー: {0}")]
81 Internal(String),
82}
83
84impl IntoResponse for ServerError {
85 fn into_response(self) -> Response {
86 let (status, error_type, code, message) = match self {
87 ServerError::BadRequest(msg) => (
88 StatusCode::BAD_REQUEST,
89 "invalid_request_error",
90 "bad_request",
91 msg,
92 ),
93 ServerError::StateCacheMiss(msg) => (
94 StatusCode::BAD_REQUEST,
95 "invalid_request_error",
96 "state_cache_miss",
97 msg,
98 ),
99 ServerError::ValidationError(core_err) => {
100 let code = match &core_err {
101 CoreError::EmptyQuestions => "empty_questions",
102 CoreError::MissingCriteria { .. } => "missing_criteria",
103 CoreError::InvalidCriteriaType { .. } => "invalid_criteria_type",
104 CoreError::InvalidChoiceCount { .. } => "invalid_choice_count",
105 CoreError::InvalidScoreLevelCount { .. } => "invalid_score_level_count",
106 _ => "schema_validation_error",
107 };
108 (
109 StatusCode::BAD_REQUEST,
110 "invalid_request_error",
111 code,
112 core_err.to_string(),
113 )
114 }
115 ServerError::PayloadTooLarge(msg) => (
116 StatusCode::PAYLOAD_TOO_LARGE,
117 "invalid_request_error",
118 "payload_too_large",
119 msg,
120 ),
121 ServerError::Runtime(runtime_err) => {
122 tracing::error!("ランタイム推論エラーが発生しました: {:?}", runtime_err);
123 (
124 StatusCode::INTERNAL_SERVER_ERROR,
125 "internal_server_error",
126 "inference_engine_error",
127 format!("推論エンジンでエラーが発生しました: {runtime_err}"),
128 )
129 }
130 ServerError::Dag(dag_err) => {
131 use sokuto_runtime::dag::DagError;
132 match dag_err {
133 DagError::Validation(v) => (
134 StatusCode::BAD_REQUEST,
135 "invalid_request_error",
136 "dag_validation_error",
137 v.to_string(),
138 ),
139 DagError::Pool(p) => (
140 StatusCode::SERVICE_UNAVAILABLE,
141 "service_unavailable",
142 "pool_error",
143 p.to_string(),
144 ),
145 DagError::Timeout(ms) => (
146 StatusCode::GATEWAY_TIMEOUT,
147 "gateway_timeout",
148 "dag_timeout",
149 format!("DAG 実行がタイムアウトしました ({ms}ms)。"),
150 ),
151 DagError::StepLimitExceeded(max) => (
152 StatusCode::BAD_REQUEST,
153 "invalid_request_error",
154 "step_limit_exceeded",
155 format!("DAG 実行ステップ数が上限 ({max}) を超過しました。"),
156 ),
157 DagError::NodeNotFound(node) => (
158 StatusCode::BAD_REQUEST,
159 "invalid_request_error",
160 "node_not_found",
161 format!("ノード '{node}' が見つかりません。"),
162 ),
163 DagError::Runtime(r) => {
164 tracing::error!("DAG 推論ランタイムエラー: {:?}", r);
165 (
166 StatusCode::INTERNAL_SERVER_ERROR,
167 "internal_server_error",
168 "inference_engine_error",
169 format!("DAG 推論エンジンでエラーが発生しました: {r}"),
170 )
171 }
172 DagError::ExecutionFailed(msg) => {
173 tracing::error!("DAG 実行失敗: {}", msg);
174 (
175 StatusCode::INTERNAL_SERVER_ERROR,
176 "internal_server_error",
177 "dag_execution_failed",
178 msg,
179 )
180 }
181 }
182 }
183 ServerError::GuardrailViolation(msg) => {
184 tracing::warn!("ガードレール違反によりリクエストを遮断しました: {}", msg);
185 (
186 StatusCode::BAD_REQUEST,
187 "invalid_request_error",
188 "guardrail_violation",
189 msg,
190 )
191 }
192 ServerError::Internal(msg) => {
193 tracing::error!("サーバー内部エラーが発生しました: {}", msg);
194 (
195 StatusCode::INTERNAL_SERVER_ERROR,
196 "internal_server_error",
197 "internal_server_error",
198 msg,
199 )
200 }
201 };
202
203 let body = ErrorResponse::new(message, error_type, code);
204 (status, axum::Json(body)).into_response()
205 }
206}