streamweave/http_server/error.rs
1//! # HTTP Error Handling
2//!
3//! This module provides comprehensive error handling for HTTP server integration,
4//! mapping StreamWeave errors to appropriate HTTP status codes and formatting
5//! error responses as JSON.
6//!
7//! ## Features
8//!
9//! - Maps StreamWeave errors to HTTP status codes
10//! - Formats error responses as structured JSON
11//! - Supports different error types (validation, not found, server errors)
12//! - Supports custom error responses
13//! - Preserves error details in development mode
14//!
15//! ## Example
16//!
17//! ```rust,no_run
18//! use streamweave::http_server::error::{map_to_http_error, ErrorResponse};
19//! use streamweave::error::StreamError;
20//! use axum::http::StatusCode;
21//!
22//! let stream_error = StreamError::new(
23//! Box::new(std::io::Error::other("Validation failed")),
24//! // ... error context ...
25//! );
26//!
27//! let http_error = map_to_http_error(&stream_error, false);
28//! assert_eq!(http_error.status, StatusCode::BAD_REQUEST);
29//! ```
30
31#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
32use crate::error::StreamError;
33#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
34use axum::http::StatusCode;
35#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
36use serde::{Deserialize, Serialize};
37#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
38use std::fmt;
39
40/// Structured error response for HTTP endpoints.
41///
42/// This type represents a standardized error response format that can be
43/// serialized to JSON and sent to clients.
44///
45/// ## Example
46///
47/// ```rust,no_run
48/// use streamweave::http_server::error::ErrorResponse;
49/// use axum::http::StatusCode;
50///
51/// let error = ErrorResponse {
52/// status: StatusCode::BAD_REQUEST,
53/// message: "Invalid input".to_string(),
54/// error: Some("ValidationError".to_string()),
55/// details: None,
56/// };
57/// ```
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
60pub struct ErrorResponse {
61 /// HTTP status code for the error
62 pub status: u16,
63 /// Human-readable error message
64 pub message: String,
65 /// Error type identifier (e.g., "ValidationError", "NotFoundError")
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub error: Option<String>,
68 /// Additional error details (only included in development mode)
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub details: Option<ErrorDetails>,
71}
72
73/// Detailed error information (only included in development mode).
74///
75/// This contains additional context about the error that may be useful
76/// for debugging but should not be exposed in production.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
79pub struct ErrorDetails {
80 /// The underlying error message
81 pub error: String,
82 /// Component name where the error occurred
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub component: Option<String>,
85 /// Timestamp when the error occurred
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub timestamp: Option<String>,
88 /// Additional context about the error
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub context: Option<String>,
91}
92
93/// Maps a StreamWeave `StreamError` to an HTTP status code.
94///
95/// This function analyzes the error and determines the most appropriate
96/// HTTP status code based on the error type and message.
97///
98/// ## Error Type Mapping
99///
100/// - **Validation errors** (e.g., "invalid", "missing", "required") → `400 Bad Request`
101/// - **Not found errors** (e.g., "not found", "missing") → `404 Not Found`
102/// - **Authentication errors** (e.g., "unauthorized", "forbidden") → `401 Unauthorized` or `403 Forbidden`
103/// - **Rate limiting errors** (e.g., "rate limit", "too many") → `429 Too Many Requests`
104/// - **Server errors** (default) → `500 Internal Server Error`
105///
106/// ## Arguments
107///
108/// * `error` - The StreamWeave error to map
109/// * `include_details` - Whether to include detailed error information (development mode)
110///
111/// ## Returns
112///
113/// An `ErrorResponse` with the appropriate status code and formatted message.
114///
115/// ## Example
116///
117/// ```rust,no_run
118/// use streamweave::http_server::error::map_to_http_error;
119/// use streamweave::error::{StreamError, ErrorContext, ComponentInfo};
120/// use axum::http::StatusCode;
121///
122/// let stream_error = StreamError::new(
123/// Box::new(std::io::Error::other("Invalid input: missing required field")),
124/// ErrorContext {
125/// timestamp: chrono::Utc::now(),
126/// item: None,
127/// component_name: "validator".to_string(),
128/// component_type: "ValidatorTransformer".to_string(),
129/// },
130/// ComponentInfo {
131/// name: "validator".to_string(),
132/// type_name: "ValidatorTransformer".to_string(),
133/// },
134/// );
135///
136/// let http_error = map_to_http_error(&stream_error, false);
137/// assert_eq!(http_error.status, StatusCode::BAD_REQUEST.as_u16());
138/// ```
139#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
140pub fn map_to_http_error<T>(error: &StreamError<T>, include_details: bool) -> ErrorResponse
141where
142 T: fmt::Debug + Clone + Send + Sync,
143{
144 let error_message = error.to_string();
145 let error_lower = error_message.to_lowercase();
146
147 // Determine status code based on error message patterns
148 let (status, error_type) = if error_lower.contains("not found")
149 || error_lower.contains("missing") && error_lower.contains("not found")
150 {
151 (StatusCode::NOT_FOUND, Some("NotFoundError".to_string()))
152 } else if error_lower.contains("unauthorized") || error_lower.contains("authentication") {
153 (
154 StatusCode::UNAUTHORIZED,
155 Some("UnauthorizedError".to_string()),
156 )
157 } else if error_lower.contains("forbidden") || error_lower.contains("permission") {
158 (StatusCode::FORBIDDEN, Some("ForbiddenError".to_string()))
159 } else if error_lower.contains("rate limit") || error_lower.contains("too many requests") {
160 (
161 StatusCode::TOO_MANY_REQUESTS,
162 Some("RateLimitError".to_string()),
163 )
164 } else if error_lower.contains("invalid")
165 || error_lower.contains("validation")
166 || error_lower.contains("bad request")
167 || error_lower.contains("malformed")
168 {
169 (StatusCode::BAD_REQUEST, Some("ValidationError".to_string()))
170 } else if error_lower.contains("conflict") || error_lower.contains("already exists") {
171 (StatusCode::CONFLICT, Some("ConflictError".to_string()))
172 } else if error_lower.contains("timeout") || error_lower.contains("timed out") {
173 (
174 StatusCode::REQUEST_TIMEOUT,
175 Some("TimeoutError".to_string()),
176 )
177 } else if error_lower.contains("not implemented") {
178 (
179 StatusCode::NOT_IMPLEMENTED,
180 Some("NotImplementedError".to_string()),
181 )
182 } else if error_lower.contains("service unavailable") || error_lower.contains("unavailable") {
183 (
184 StatusCode::SERVICE_UNAVAILABLE,
185 Some("ServiceUnavailableError".to_string()),
186 )
187 } else {
188 // Default to internal server error
189 (
190 StatusCode::INTERNAL_SERVER_ERROR,
191 Some("InternalServerError".to_string()),
192 )
193 };
194
195 // Build error details if in development mode
196 let details = if include_details {
197 Some(ErrorDetails {
198 error: error_message.clone(),
199 component: Some(error.context.component_name.clone()),
200 timestamp: Some(error.context.timestamp.to_rfc3339()),
201 context: Some(format!("{:?}", error.context)),
202 })
203 } else {
204 None
205 };
206
207 ErrorResponse {
208 status: status.as_u16(),
209 message: error_message,
210 error: error_type,
211 details,
212 }
213}
214
215/// Maps a generic error to an HTTP error response.
216///
217/// This is a convenience function for converting any error type to an
218/// HTTP error response. It wraps the error in a `StreamError` and then
219/// maps it to an HTTP status code.
220///
221/// ## Arguments
222///
223/// * `error` - The error to convert
224/// * `status` - Optional HTTP status code (defaults to 500 if not provided)
225/// * `include_details` - Whether to include detailed error information
226///
227/// ## Returns
228///
229/// An `ErrorResponse` with the appropriate status code and message.
230///
231/// ## Example
232///
233/// ```rust,no_run
234/// use streamweave::http_server::error::map_generic_error;
235/// use axum::http::StatusCode;
236///
237/// let error = std::io::Error::other("Something went wrong");
238/// let http_error = map_generic_error(&error, Some(StatusCode::BAD_REQUEST), false);
239/// ```
240#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
241pub fn map_generic_error(
242 error: &dyn std::error::Error,
243 status: Option<StatusCode>,
244 include_details: bool,
245) -> ErrorResponse {
246 let status = status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
247 let message = error.to_string();
248
249 let details = if include_details {
250 Some(ErrorDetails {
251 error: format!("{:?}", error),
252 component: None,
253 timestamp: Some(chrono::Utc::now().to_rfc3339()),
254 context: None,
255 })
256 } else {
257 None
258 };
259
260 ErrorResponse {
261 status: status.as_u16(),
262 message,
263 error: Some("Error".to_string()),
264 details,
265 }
266}
267
268/// Creates a custom error response with a specific message and status code.
269///
270/// This function allows you to create custom error responses for specific
271/// error conditions in your application.
272///
273/// ## Arguments
274///
275/// * `status` - HTTP status code
276/// * `message` - Error message
277/// * `error_type` - Optional error type identifier
278/// * `include_details` - Whether to include detailed error information
279///
280/// ## Returns
281///
282/// An `ErrorResponse` with the specified status code and message.
283///
284/// ## Example
285///
286/// ```rust,no_run
287/// use streamweave::http_server::error::create_custom_error;
288/// use axum::http::StatusCode;
289///
290/// let error = create_custom_error(
291/// StatusCode::BAD_REQUEST,
292/// "Invalid user ID format",
293/// Some("InvalidUserIdError".to_string()),
294/// false,
295/// );
296/// ```
297#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
298pub fn create_custom_error(
299 status: StatusCode,
300 message: impl Into<String>,
301 error_type: Option<String>,
302 include_details: bool,
303) -> ErrorResponse {
304 let message_str = message.into();
305 ErrorResponse {
306 status: status.as_u16(),
307 message: message_str.clone(),
308 error: error_type,
309 details: if include_details {
310 Some(ErrorDetails {
311 error: message_str,
312 component: None,
313 timestamp: Some(chrono::Utc::now().to_rfc3339()),
314 context: None,
315 })
316 } else {
317 None
318 },
319 }
320}
321
322/// Determines if the application is running in development mode.
323///
324/// This checks the `RUST_ENV` or `ENVIRONMENT` environment variable
325/// to determine if detailed error information should be included.
326///
327/// ## Returns
328///
329/// `true` if in development mode, `false` otherwise.
330#[cfg(all(not(target_arch = "wasm32"), feature = "http-server"))]
331pub fn is_development_mode() -> bool {
332 std::env::var("RUST_ENV")
333 .or_else(|_| std::env::var("ENVIRONMENT"))
334 .map(|env| env.to_lowercase() == "development" || env.to_lowercase() == "dev")
335 .unwrap_or(false)
336}