reasonkit_web/stripe/
error.rs1use axum::{
6 http::StatusCode,
7 response::{IntoResponse, Response},
8 Json,
9};
10use serde::Serialize;
11use thiserror::Error;
12
13#[derive(Error, Debug)]
15pub enum StripeWebhookError {
16 #[error("STRIPE_WEBHOOK_SECRET environment variable not set")]
21 MissingSecret,
22
23 #[error("Invalid webhook secret format: {0}")]
25 InvalidSecretFormat(String),
26
27 #[error("Missing stripe-signature header")]
32 MissingSignature,
33
34 #[error("Invalid signature format: {0}")]
36 InvalidSignatureFormat(String),
37
38 #[error("Signature verification failed")]
40 SignatureVerificationFailed,
41
42 #[error("Webhook timestamp too old: {age_seconds}s (max: {max_age_seconds}s)")]
44 TimestampTooOld {
45 age_seconds: i64,
46 max_age_seconds: i64,
47 },
48
49 #[error("Webhook timestamp in future by {drift_seconds}s")]
51 TimestampInFuture { drift_seconds: i64 },
52
53 #[error("Failed to parse request body: {0}")]
58 InvalidPayload(String),
59
60 #[error("Unknown event type: {0}")]
62 UnknownEventType(String),
63
64 #[error("Missing required field: {0}")]
66 MissingField(String),
67
68 #[error("Event {event_id} already processed")]
73 AlreadyProcessed { event_id: String },
74
75 #[error("Event processing failed: {0}")]
80 ProcessingFailed(String),
81
82 #[error("Database error: {0}")]
84 DatabaseError(String),
85
86 #[error("External service error: {0}")]
88 ExternalServiceError(String),
89
90 #[error("Internal error: {0}")]
95 InternalError(String),
96}
97
98pub type StripeWebhookResult<T> = std::result::Result<T, StripeWebhookError>;
100
101impl StripeWebhookError {
102 pub fn status_code(&self) -> StatusCode {
104 match self {
105 Self::InvalidPayload(_)
107 | Self::InvalidSignatureFormat(_)
108 | Self::UnknownEventType(_)
109 | Self::MissingField(_) => StatusCode::BAD_REQUEST,
110
111 Self::MissingSignature
113 | Self::SignatureVerificationFailed
114 | Self::TimestampTooOld { .. }
115 | Self::TimestampInFuture { .. } => StatusCode::UNAUTHORIZED,
116
117 Self::AlreadyProcessed { .. } => StatusCode::ACCEPTED,
119
120 Self::MissingSecret
122 | Self::InvalidSecretFormat(_)
123 | Self::ProcessingFailed(_)
124 | Self::DatabaseError(_)
125 | Self::ExternalServiceError(_)
126 | Self::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
127 }
128 }
129
130 pub fn should_retry(&self) -> bool {
135 matches!(
136 self.status_code(),
137 StatusCode::INTERNAL_SERVER_ERROR | StatusCode::SERVICE_UNAVAILABLE
138 )
139 }
140
141 pub fn error_code(&self) -> &'static str {
143 match self {
144 Self::MissingSecret => "MISSING_SECRET",
145 Self::InvalidSecretFormat(_) => "INVALID_SECRET_FORMAT",
146 Self::MissingSignature => "MISSING_SIGNATURE",
147 Self::InvalidSignatureFormat(_) => "INVALID_SIGNATURE_FORMAT",
148 Self::SignatureVerificationFailed => "SIGNATURE_VERIFICATION_FAILED",
149 Self::TimestampTooOld { .. } => "TIMESTAMP_TOO_OLD",
150 Self::TimestampInFuture { .. } => "TIMESTAMP_IN_FUTURE",
151 Self::InvalidPayload(_) => "INVALID_PAYLOAD",
152 Self::UnknownEventType(_) => "UNKNOWN_EVENT_TYPE",
153 Self::MissingField(_) => "MISSING_FIELD",
154 Self::AlreadyProcessed { .. } => "ALREADY_PROCESSED",
155 Self::ProcessingFailed(_) => "PROCESSING_FAILED",
156 Self::DatabaseError(_) => "DATABASE_ERROR",
157 Self::ExternalServiceError(_) => "EXTERNAL_SERVICE_ERROR",
158 Self::InternalError(_) => "INTERNAL_ERROR",
159 }
160 }
161}
162
163#[derive(Debug, Clone, Serialize)]
165pub struct ErrorResponse {
166 pub error: ErrorDetails,
167}
168
169#[derive(Debug, Clone, Serialize)]
170pub struct ErrorDetails {
171 pub code: String,
172 pub message: String,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub retry_after: Option<u64>,
175}
176
177impl IntoResponse for StripeWebhookError {
178 fn into_response(self) -> Response {
179 let status = self.status_code();
180 let error_code = self.error_code().to_string();
181
182 let message = match &self {
184 Self::MissingSignature => self.to_string(),
186 Self::InvalidSignatureFormat(_) => "Invalid signature format".to_string(),
187 Self::SignatureVerificationFailed => self.to_string(),
188 Self::TimestampTooOld { .. } | Self::TimestampInFuture { .. } => {
189 "Webhook timestamp validation failed".to_string()
190 }
191 Self::InvalidPayload(_) => "Invalid request payload".to_string(),
192 Self::UnknownEventType(t) => format!("Unknown event type: {}", t),
193 Self::MissingField(f) => format!("Missing required field: {}", f),
194 Self::AlreadyProcessed { event_id } => {
195 format!("Event {} already processed", event_id)
196 }
197 Self::MissingSecret
199 | Self::InvalidSecretFormat(_)
200 | Self::ProcessingFailed(_)
201 | Self::DatabaseError(_)
202 | Self::ExternalServiceError(_)
203 | Self::InternalError(_) => "Internal server error".to_string(),
204 };
205
206 let body = ErrorResponse {
207 error: ErrorDetails {
208 code: error_code,
209 message,
210 retry_after: if self.should_retry() {
211 Some(60) } else {
213 None
214 },
215 },
216 };
217
218 (status, Json(body)).into_response()
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_error_status_codes() {
228 assert_eq!(
229 StripeWebhookError::MissingSignature.status_code(),
230 StatusCode::UNAUTHORIZED
231 );
232 assert_eq!(
233 StripeWebhookError::InvalidPayload("test".to_string()).status_code(),
234 StatusCode::BAD_REQUEST
235 );
236 assert_eq!(
237 StripeWebhookError::AlreadyProcessed {
238 event_id: "evt_123".to_string()
239 }
240 .status_code(),
241 StatusCode::ACCEPTED
242 );
243 assert_eq!(
244 StripeWebhookError::ProcessingFailed("test".to_string()).status_code(),
245 StatusCode::INTERNAL_SERVER_ERROR
246 );
247 }
248
249 #[test]
250 fn test_should_retry() {
251 assert!(!StripeWebhookError::MissingSignature.should_retry());
252 assert!(!StripeWebhookError::InvalidPayload("test".to_string()).should_retry());
253 assert!(StripeWebhookError::ProcessingFailed("test".to_string()).should_retry());
254 assert!(StripeWebhookError::DatabaseError("test".to_string()).should_retry());
255 }
256
257 #[test]
258 fn test_error_codes() {
259 assert_eq!(
260 StripeWebhookError::MissingSignature.error_code(),
261 "MISSING_SIGNATURE"
262 );
263 assert_eq!(
264 StripeWebhookError::SignatureVerificationFailed.error_code(),
265 "SIGNATURE_VERIFICATION_FAILED"
266 );
267 }
268}