cloudillo_core/rate_limit/
error.rs1use std::time::Duration;
6
7use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9use axum::Json;
10
11#[derive(Debug)]
13pub enum RateLimitError {
14 RateLimited {
16 level: &'static str,
18 retry_after: Duration,
20 },
21 Banned {
23 remaining: Option<Duration>,
25 },
26 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 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#[derive(Debug)]
107pub enum PowError {
108 InsufficientWork {
110 required: u32,
112 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 (StatusCode::PRECONDITION_REQUIRED, Json(body)).into_response()
150 }
151 }
152 }
153}
154
155