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}