Skip to main content

road_runner_common/
error.rs

1use crate::response::{ApiErrorDetail, ApiResponse};
2use thiserror::Error;
3
4#[derive(Debug, Clone, Copy)]
5pub enum ErrorCode {
6    Validation,
7    Unauthorized,
8    Forbidden,
9    NotFound,
10    Conflict,
11    RateLimited,
12    Internal,
13    External,
14}
15
16impl ErrorCode {
17    pub fn as_str(self) -> &'static str {
18        match self {
19            ErrorCode::Validation => "VALIDATION_ERROR",
20            ErrorCode::Unauthorized => "UNAUTHORIZED",
21            ErrorCode::Forbidden => "FORBIDDEN",
22            ErrorCode::NotFound => "NOT_FOUND",
23            ErrorCode::Conflict => "CONFLICT",
24            ErrorCode::RateLimited => "RATE_LIMITED",
25            ErrorCode::Internal => "INTERNAL_ERROR",
26            ErrorCode::External => "EXTERNAL_ERROR",
27        }
28    }
29}
30
31#[derive(Error, Debug)]
32pub enum AppError {
33    #[error("Validation failed")]
34    Validation {
35        message: String,
36        #[source]
37        source: Option<anyhow::Error>,
38        details: Vec<ApiErrorDetail>,
39    },
40
41    #[error("Unauthorized: {message}")]
42    Unauthorized { message: String },
43
44    #[error("Forbidden: {message}")]
45    Forbidden { message: String },
46
47    #[error("Not found: {message}")]
48    NotFound { message: String },
49
50    #[error("Conflict: {message}")]
51    Conflict { message: String },
52
53    #[error("Rate limited: {message}")]
54    RateLimited { message: String },
55
56    #[error("Internal error")]
57    Internal {
58        message: String,
59        #[source]
60        source: Option<anyhow::Error>,
61    },
62
63    #[error("External dependency error")]
64    External {
65        message: String,
66        #[source]
67        source: Option<anyhow::Error>,
68    },
69}
70
71impl AppError {
72    pub fn code(&self) -> ErrorCode {
73        match self {
74            AppError::Validation { .. } => ErrorCode::Validation,
75            AppError::Unauthorized { .. } => ErrorCode::Unauthorized,
76            AppError::Forbidden { .. } => ErrorCode::Forbidden,
77            AppError::NotFound { .. } => ErrorCode::NotFound,
78            AppError::Conflict { .. } => ErrorCode::Conflict,
79            AppError::RateLimited { .. } => ErrorCode::RateLimited,
80            AppError::Internal { .. } => ErrorCode::Internal,
81            AppError::External { .. } => ErrorCode::External,
82        }
83    }
84
85    pub fn public_message(&self) -> String {
86        match self {
87            AppError::Validation { message, .. } => message.clone(),
88            AppError::Unauthorized { message } => message.clone(),
89            AppError::Forbidden { message } => message.clone(),
90            AppError::NotFound { message } => message.clone(),
91            AppError::Conflict { message } => message.clone(),
92            AppError::RateLimited { message } => message.clone(),
93            AppError::Internal { .. } => "Internal server error".to_string(),
94            AppError::External { .. } => "Upstream service error".to_string(),
95        }
96    }
97
98    pub fn validation(message: impl Into<String>, details: Vec<ApiErrorDetail>) -> Self {
99        Self::Validation {
100            message: message.into(),
101            source: None,
102            details,
103        }
104    }
105
106    pub fn internal(message: impl Into<String>, source: Option<anyhow::Error>) -> Self {
107        Self::Internal {
108            message: message.into(),
109            source,
110        }
111    }
112
113    pub fn external(message: impl Into<String>, source: Option<anyhow::Error>) -> Self {
114        Self::External {
115            message: message.into(),
116            source,
117        }
118    }
119
120    pub fn to_api_response(&self, request_id: impl Into<String>) -> ApiResponse<()> {
121        let rid = request_id.into();
122        match self {
123            AppError::Validation { details, .. } => {
124                let mut errs = details.clone();
125                if errs.is_empty() {
126                    errs.push(ApiErrorDetail::new(self.code().as_str(), self.public_message()));
127                }
128                ApiResponse::err(errs, rid)
129            }
130            _ => ApiResponse::err(
131                vec![ApiErrorDetail::new(self.code().as_str(), self.public_message())],
132                rid,
133            ),
134        }
135    }
136}
137
138#[cfg(feature = "web")]
139mod web_impl {
140    use super::*;
141    use actix_web::{http::StatusCode, HttpResponse, ResponseError};
142
143    impl AppError {
144        pub fn http_status(&self) -> StatusCode {
145            match self {
146                AppError::Validation { .. } => StatusCode::BAD_REQUEST,
147                AppError::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
148                AppError::Forbidden { .. } => StatusCode::FORBIDDEN,
149                AppError::NotFound { .. } => StatusCode::NOT_FOUND,
150                AppError::Conflict { .. } => StatusCode::CONFLICT,
151                AppError::RateLimited { .. } => StatusCode::TOO_MANY_REQUESTS,
152                AppError::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
153                AppError::External { .. } => StatusCode::BAD_GATEWAY,
154            }
155        }
156    }
157
158    /// Actix: `Result<T, AppError>` döndüğünde otomatik olarak buradan HTTP response üretir.
159    impl ResponseError for AppError {
160        fn status_code(&self) -> StatusCode {
161            self.http_status()
162        }
163
164        fn error_response(&self) -> HttpResponse {
165            // request_id propagation: idealde middleware header/extension ile koyar.
166            // Burada fallback üretiyoruz.
167            let request_id = uuid::Uuid::new_v4().to_string();
168
169            #[cfg(feature = "observability")]
170            tracing::error!(error_code = %self.code().as_str(), error = ?self, "request failed");
171
172            let body = self.to_api_response(request_id);
173            HttpResponse::build(self.status_code()).json(body)
174        }
175    }
176}