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