road_runner_common/
error.rs1use 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 impl ResponseError for AppError {
160 fn status_code(&self) -> StatusCode {
161 self.http_status()
162 }
163
164 fn error_response(&self) -> HttpResponse {
165 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}