Skip to main content

cloudillo_core/rate_limit/
error.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! Rate Limiting Error Types
5//!
6//! Error types for rate limiting and proof-of-work failures.
7
8use std::time::Duration;
9
10use axum::http::StatusCode;
11use axum::response::{IntoResponse, Response};
12use axum::Json;
13
14/// Rate limit error types
15#[derive(Debug)]
16pub enum RateLimitError {
17	/// Request rate limited at a specific hierarchical level
18	RateLimited {
19		/// Which address level triggered the limit
20		level: &'static str,
21		/// Time until limit resets
22		retry_after: Duration,
23	},
24	/// Address is banned
25	Banned {
26		/// Remaining ban duration
27		remaining: Option<Duration>,
28	},
29	/// Unknown rate limit category
30	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				// Add standard rate limit headers
74				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/// Proof-of-work error types
109#[derive(Debug)]
110pub enum PowError {
111	/// Insufficient proof-of-work provided
112	InsufficientWork {
113		/// Required number of 'A' characters
114		required: u32,
115		/// The required suffix string
116		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				// HTTP 428 Precondition Required
152				(StatusCode::PRECONDITION_REQUIRED, Json(body)).into_response()
153			}
154		}
155	}
156}
157
158// vim: ts=4