Skip to main content

cloudillo_core/rate_limit/
error.rs

1//! Rate Limiting Error Types
2//!
3//! Error types for rate limiting and proof-of-work failures.
4
5use std::time::Duration;
6
7use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9use axum::Json;
10
11/// Rate limit error types
12#[derive(Debug)]
13pub enum RateLimitError {
14	/// Request rate limited at a specific hierarchical level
15	RateLimited {
16		/// Which address level triggered the limit
17		level: &'static str,
18		/// Time until limit resets
19		retry_after: Duration,
20	},
21	/// Address is banned
22	Banned {
23		/// Remaining ban duration
24		remaining: Option<Duration>,
25	},
26	/// Unknown rate limit category
27	UnknownCategory(String),
28}
29
30impl std::fmt::Display for RateLimitError {
31	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32		match self {
33			RateLimitError::RateLimited { level, retry_after } => {
34				write!(f, "Rate limited at {} level, retry after {:?}", level, retry_after)
35			}
36			RateLimitError::Banned { remaining } => {
37				if let Some(dur) = remaining {
38					write!(f, "Address banned for {:?}", dur)
39				} else {
40					write!(f, "Address banned permanently")
41				}
42			}
43			RateLimitError::UnknownCategory(cat) => {
44				write!(f, "Unknown rate limit category: {}", cat)
45			}
46		}
47	}
48}
49
50impl std::error::Error for RateLimitError {}
51
52impl IntoResponse for RateLimitError {
53	fn into_response(self) -> Response {
54		match self {
55			RateLimitError::RateLimited { level, retry_after } => {
56				let retry_secs = retry_after.as_secs();
57				let body = serde_json::json!({
58					"error": {
59						"code": "E-RATE-LIMITED",
60						"message": "Too many requests. Please slow down.",
61						"details": {
62							"level": level,
63							"retryAfter": retry_secs
64						}
65					}
66				});
67
68				let mut response = (StatusCode::TOO_MANY_REQUESTS, Json(body)).into_response();
69
70				// Add standard rate limit headers
71				if let Ok(val) = retry_secs.to_string().parse() {
72					response.headers_mut().insert("Retry-After", val);
73				}
74				if let Ok(val) = level.parse() {
75					response.headers_mut().insert("X-RateLimit-Level", val);
76				}
77
78				response
79			}
80			RateLimitError::Banned { remaining } => {
81				let body = serde_json::json!({
82					"error": {
83						"code": "E-RATE-BANNED",
84						"message": "Access temporarily blocked due to repeated violations.",
85						"details": {
86							"remainingSecs": remaining.map(|d| d.as_secs())
87						}
88					}
89				});
90				(StatusCode::FORBIDDEN, Json(body)).into_response()
91			}
92			RateLimitError::UnknownCategory(_) => {
93				let body = serde_json::json!({
94					"error": {
95						"code": "E-INTERNAL",
96						"message": "Internal rate limit error"
97					}
98				});
99				(StatusCode::INTERNAL_SERVER_ERROR, Json(body)).into_response()
100			}
101		}
102	}
103}
104
105/// Proof-of-work error types
106#[derive(Debug)]
107pub enum PowError {
108	/// Insufficient proof-of-work provided
109	InsufficientWork {
110		/// Required number of 'A' characters
111		required: u32,
112		/// The required suffix string
113		suffix: String,
114	},
115}
116
117impl std::fmt::Display for PowError {
118	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119		match self {
120			PowError::InsufficientWork { required, suffix } => {
121				write!(
122					f,
123					"Proof of work required: token must end with '{}' ({} chars)",
124					suffix, required
125				)
126			}
127		}
128	}
129}
130
131impl std::error::Error for PowError {}
132
133impl IntoResponse for PowError {
134	fn into_response(self) -> Response {
135		match self {
136			PowError::InsufficientWork { required, suffix } => {
137				let body = serde_json::json!({
138					"error": {
139						"code": "E-POW-REQUIRED",
140						"message": "Proof of work required for this action",
141						"details": {
142							"required": required,
143							"postfix": suffix,
144							"hint": format!("Action token must end with '{}'", suffix)
145						}
146					}
147				});
148				// HTTP 428 Precondition Required
149				(StatusCode::PRECONDITION_REQUIRED, Json(body)).into_response()
150			}
151		}
152	}
153}
154
155// vim: ts=4