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