Skip to main content

sokuto_server/
error.rs

1//! # エラーハンドリングモジュール
2//!
3//! HTTP レスポンスへのマッピングおよび Jev 互換 JSON エラーペイロードを定義する。
4
5use 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/// Jev 互換のエラー詳細情報。
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
14pub struct ErrorDetail {
15    /// エラー内容の人間向け説明文。
16    pub message: String,
17    /// エラーの種別分類(`invalid_request_error`, `internal_server_error` 等)。
18    #[serde(rename = "type")]
19    pub error_type: String,
20    /// 固有のエラー識別コード。
21    pub code: String,
22}
23
24/// Jev 互換エラーレスポンス構造体。
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
26pub struct ErrorResponse {
27    /// エラー詳細ペイロード。
28    pub error: ErrorDetail,
29}
30
31impl ErrorResponse {
32    /// 新規エラーレスポンスを生成する。
33    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/// サーバー内部で発生する各種エラーの統括型。
49#[derive(Debug, thiserror::Error)]
50pub enum ServerError {
51    /// 不正なリクエスト構文またはパラメータ。
52    #[error("不正なリクエスト: {0}")]
53    BadRequest(String),
54
55    /// Core スキーマの整合性検証エラー。
56    #[error("スキーマ検証エラー: {0}")]
57    ValidationError(#[from] CoreError),
58
59    /// 質問数またはペイロードサイズの上限超過。
60    #[error("リソース上限超過: {0}")]
61    PayloadTooLarge(String),
62
63    /// ランタイム推論エンジン障害。
64    #[error("推論エンジン障害: {0}")]
65    Runtime(#[from] RuntimeError),
66
67    /// DAG 実行エラー。
68    #[error("DAG 実行エラー: {0}")]
69    Dag(#[from] sokuto_runtime::dag::DagError),
70
71    /// ガードレール違反 (プロンプトインジェクション、禁止語、ハルシネーション等)。
72    #[error("ガードレール違反: {0}")]
73    GuardrailViolation(String),
74
75    /// State キャッシュミス (State-Offloading で State が空だがキャッシュが存在しない)。
76    #[error("State キャッシュミス: {0}")]
77    StateCacheMiss(String),
78
79    /// サーバー内部の不整合・障害。
80    #[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}