axum_conf/
error.rs

1//! Error types and handling for the Axum service library.
2//!
3//! This module provides structured error responses with unique error codes and
4//! automatic HTTP status code mapping. All errors implement `IntoResponse` and
5//! automatically serialize to JSON.
6//!
7//! # Design
8//!
9//! This module uses an opaque `Error` struct paired with an `ErrorKind` enum,
10//! following the `std::io::Error` pattern. This design provides API stability:
11//! internal error sources can change without breaking consumers.
12//!
13//! # Example
14//!
15//! ```rust
16//! use axum_conf::{Error, ErrorKind};
17//!
18//! // Create errors using convenience constructors
19//! let error = Error::internal("Something went wrong");
20//!
21//! // Match on the error kind
22//! match error.kind() {
23//!     ErrorKind::Internal => println!("Internal error: {}", error),
24//!     ErrorKind::Database => println!("Database error: {}", error),
25//!     _ => println!("Other error: {}", error),
26//! }
27//!
28//! // Get the HTTP status code
29//! use axum::http::StatusCode;
30//! assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
31//! ```
32
33use axum::{
34    Json,
35    http::StatusCode,
36    response::{IntoResponse, Response},
37};
38use serde::Serialize;
39use std::fmt;
40use thiserror::Error;
41
42/// The kind of error that occurred.
43///
44/// This enum categorizes errors for matching purposes. Use `Error::kind()`
45/// to get the kind of an error.
46///
47/// # Stability
48///
49/// This enum is marked `#[non_exhaustive]`, so new variants may be added
50/// in future versions without breaking existing code. Always include a
51/// wildcard arm when matching.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
53#[non_exhaustive]
54pub enum ErrorKind {
55    /// Database error (connection, query, pool issues).
56    #[error("database error")]
57    Database,
58
59    /// Authentication or authorization error.
60    #[error("authentication error")]
61    Authentication,
62
63    /// Configuration error (invalid TOML, missing values).
64    #[error("configuration error")]
65    Configuration,
66
67    /// TLS/certificate error.
68    #[error("TLS error")]
69    Tls,
70
71    /// I/O error (file operations, network).
72    #[error("I/O error")]
73    Io,
74
75    /// Invalid input (bad URL, header, request data).
76    #[error("invalid input")]
77    InvalidInput,
78
79    /// Circuit breaker is open, rejecting requests.
80    #[error("circuit breaker open")]
81    CircuitBreakerOpen,
82
83    /// Call through circuit breaker failed.
84    #[error("circuit breaker call failed")]
85    CircuitBreakerFailed,
86
87    /// Internal/unexpected error.
88    #[error("internal error")]
89    Internal,
90}
91
92/// An error that can occur in the axum-conf library.
93///
94/// This is an opaque error type that wraps an underlying error source.
95/// Use [`Error::kind()`] to determine the category of error for matching,
96/// and the `Display` implementation to get a human-readable message.
97///
98/// # Creating Errors
99///
100/// Use the convenience constructors for common cases:
101///
102/// ```rust
103/// use axum_conf::Error;
104///
105/// let err = Error::internal("unexpected state");
106/// let err = Error::invalid_input("missing required field");
107/// let err = Error::database("connection timeout");
108/// ```
109///
110/// Or use [`Error::new()`] for full control:
111///
112/// ```rust
113/// use axum_conf::{Error, ErrorKind};
114///
115/// let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
116/// let err = Error::new(ErrorKind::Io, io_err);
117/// ```
118pub struct Error {
119    kind: ErrorKind,
120    source: Box<dyn std::error::Error + Send + Sync + 'static>,
121}
122
123impl Error {
124    /// Creates a new error with the given kind and source.
125    ///
126    /// # Example
127    ///
128    /// ```rust
129    /// use axum_conf::{Error, ErrorKind};
130    ///
131    /// let err = Error::new(ErrorKind::Internal, "something went wrong");
132    /// assert_eq!(err.kind(), ErrorKind::Internal);
133    /// ```
134    pub fn new<E>(kind: ErrorKind, error: E) -> Self
135    where
136        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
137    {
138        Self {
139            kind,
140            source: error.into(),
141        }
142    }
143
144    /// Returns the kind of this error.
145    ///
146    /// Use this to match on error categories:
147    ///
148    /// ```rust
149    /// use axum_conf::{Error, ErrorKind};
150    ///
151    /// fn handle_error(err: Error) {
152    ///     match err.kind() {
153    ///         ErrorKind::Database => eprintln!("Database issue, will retry"),
154    ///         ErrorKind::InvalidInput => eprintln!("Bad request"),
155    ///         _ => eprintln!("Unexpected error"),
156    ///     }
157    /// }
158    /// ```
159    pub fn kind(&self) -> ErrorKind {
160        self.kind
161    }
162
163    /// Returns the error code string for this error.
164    ///
165    /// This is a stable identifier suitable for client-side error handling.
166    pub fn error_code(&self) -> &'static str {
167        match self.kind {
168            ErrorKind::Database => "DATABASE_ERROR",
169            ErrorKind::Authentication => "AUTH_ERROR",
170            ErrorKind::Configuration => "CONFIG_ERROR",
171            ErrorKind::Tls => "TLS_ERROR",
172            ErrorKind::Io => "IO_ERROR",
173            ErrorKind::InvalidInput => "INVALID_INPUT",
174            ErrorKind::CircuitBreakerOpen => "CIRCUIT_BREAKER_OPEN",
175            ErrorKind::CircuitBreakerFailed => "CIRCUIT_BREAKER_CALL_FAILED",
176            ErrorKind::Internal => "INTERNAL_ERROR",
177        }
178    }
179
180    /// Returns the HTTP status code for this error.
181    pub fn status_code(&self) -> StatusCode {
182        match self.kind {
183            ErrorKind::Database => StatusCode::SERVICE_UNAVAILABLE,
184            ErrorKind::Authentication => StatusCode::UNAUTHORIZED,
185            ErrorKind::Configuration => StatusCode::INTERNAL_SERVER_ERROR,
186            ErrorKind::Tls => StatusCode::INTERNAL_SERVER_ERROR,
187            ErrorKind::Io => StatusCode::INTERNAL_SERVER_ERROR,
188            ErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
189            ErrorKind::CircuitBreakerOpen => StatusCode::SERVICE_UNAVAILABLE,
190            ErrorKind::CircuitBreakerFailed => StatusCode::BAD_GATEWAY,
191            ErrorKind::Internal => StatusCode::INTERNAL_SERVER_ERROR,
192        }
193    }
194
195    /// Converts the error into a structured error response.
196    pub fn to_error_response(&self) -> ErrorResponse {
197        ErrorResponse::new(self.error_code(), self.to_string())
198    }
199
200    /// Consumes the error and returns the inner error source.
201    pub fn into_inner(self) -> Box<dyn std::error::Error + Send + Sync + 'static> {
202        self.source
203    }
204}
205
206// ============================================================================
207// Convenience constructors
208// ============================================================================
209
210impl Error {
211    /// Creates a database error.
212    pub fn database(msg: impl Into<String>) -> Self {
213        Self::new(ErrorKind::Database, msg.into())
214    }
215
216    /// Creates a database configuration error.
217    ///
218    /// This is a convenience method that creates a `Database` kind error
219    /// with a "Database configuration error" prefix.
220    pub fn database_config(msg: impl Into<String>) -> Self {
221        Self::new(
222            ErrorKind::Database,
223            format!("Database configuration error: {}", msg.into()),
224        )
225    }
226
227    /// Creates an authentication error.
228    pub fn authentication(msg: impl Into<String>) -> Self {
229        Self::new(ErrorKind::Authentication, msg.into())
230    }
231
232    /// Creates a configuration error.
233    pub fn config(msg: impl Into<String>) -> Self {
234        Self::new(ErrorKind::Configuration, msg.into())
235    }
236
237    /// Creates a TLS error.
238    pub fn tls(msg: impl Into<String>) -> Self {
239        Self::new(ErrorKind::Tls, msg.into())
240    }
241
242    /// Creates an I/O error from a message.
243    pub fn io(msg: impl Into<String>) -> Self {
244        Self::new(ErrorKind::Io, msg.into())
245    }
246
247    /// Creates an I/O error from a `std::io::Error`.
248    pub fn from_io(err: std::io::Error) -> Self {
249        Self::new(ErrorKind::Io, err)
250    }
251
252    /// Creates an invalid input error.
253    pub fn invalid_input(msg: impl Into<String>) -> Self {
254        Self::new(ErrorKind::InvalidInput, msg.into())
255    }
256
257    /// Creates a circuit breaker open error.
258    #[cfg(feature = "circuit-breaker")]
259    pub fn circuit_breaker_open(target: impl Into<String>) -> Self {
260        Self::new(
261            ErrorKind::CircuitBreakerOpen,
262            format!("Circuit breaker open for target: {}", target.into()),
263        )
264    }
265
266    /// Creates a circuit breaker call failed error.
267    #[cfg(feature = "circuit-breaker")]
268    pub fn circuit_breaker_failed(msg: impl Into<String>) -> Self {
269        Self::new(ErrorKind::CircuitBreakerFailed, msg.into())
270    }
271
272    /// Creates an internal error.
273    pub fn internal(msg: impl Into<String>) -> Self {
274        Self::new(ErrorKind::Internal, msg.into())
275    }
276}
277
278// ============================================================================
279// Trait implementations
280// ============================================================================
281
282impl fmt::Debug for Error {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        f.debug_struct("Error")
285            .field("kind", &self.kind)
286            .field("source", &self.source)
287            .finish()
288    }
289}
290
291impl fmt::Display for Error {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        write!(f, "{}", self.source)
294    }
295}
296
297impl std::error::Error for Error {
298    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
299        Some(&*self.source)
300    }
301}
302
303impl IntoResponse for Error {
304    fn into_response(self) -> Response {
305        let status = self.status_code();
306        let error_response = self.to_error_response();
307
308        tracing::error!(
309            error_code = %error_response.error_code,
310            message = %error_response.message,
311            status = %status.as_u16(),
312            "Error occurred"
313        );
314
315        (status, Json(error_response)).into_response()
316    }
317}
318
319// ============================================================================
320// From implementations
321// ============================================================================
322
323impl From<std::io::Error> for Error {
324    fn from(err: std::io::Error) -> Self {
325        Self::new(ErrorKind::Io, err)
326    }
327}
328
329impl From<toml::de::Error> for Error {
330    fn from(err: toml::de::Error) -> Self {
331        Self::new(ErrorKind::Configuration, err)
332    }
333}
334
335impl From<url::ParseError> for Error {
336    fn from(err: url::ParseError) -> Self {
337        Self::new(ErrorKind::InvalidInput, err)
338    }
339}
340
341impl From<std::env::VarError> for Error {
342    fn from(err: std::env::VarError) -> Self {
343        Self::new(ErrorKind::Configuration, err)
344    }
345}
346
347impl From<http::header::InvalidHeaderValue> for Error {
348    fn from(err: http::header::InvalidHeaderValue) -> Self {
349        Self::new(ErrorKind::InvalidInput, err)
350    }
351}
352
353#[cfg(feature = "postgres")]
354impl From<sqlx::Error> for Error {
355    fn from(err: sqlx::Error) -> Self {
356        Self::new(ErrorKind::Database, err)
357    }
358}
359
360#[cfg(feature = "rustls")]
361impl From<rustls::Error> for Error {
362    fn from(err: rustls::Error) -> Self {
363        Self::new(ErrorKind::Tls, err)
364    }
365}
366
367#[cfg(feature = "keycloak")]
368impl From<axum_keycloak_auth::error::AuthError> for Error {
369    fn from(err: axum_keycloak_auth::error::AuthError) -> Self {
370        Self::new(ErrorKind::Authentication, err)
371    }
372}
373
374// ============================================================================
375// ErrorResponse
376// ============================================================================
377
378/// Structured error response with error code and details.
379#[derive(Debug, Serialize)]
380#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
381pub struct ErrorResponse {
382    /// Unique error code for client-side error handling.
383    pub error_code: String,
384    /// Human-readable error message.
385    pub message: String,
386    /// Optional additional details about the error.
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub details: Option<String>,
389}
390
391impl ErrorResponse {
392    /// Creates a new error response.
393    pub fn new(error_code: impl Into<String>, message: impl Into<String>) -> Self {
394        Self {
395            error_code: error_code.into(),
396            message: message.into(),
397            details: None,
398        }
399    }
400
401    /// Adds details to the error response.
402    pub fn with_details(mut self, details: impl Into<String>) -> Self {
403        self.details = Some(details.into());
404        self
405    }
406}
407
408// ============================================================================
409// Tests
410// ============================================================================
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use std::error::Error as StdError;
416
417    // ========================================================================
418    // ErrorKind tests
419    // ========================================================================
420
421    #[test]
422    fn test_error_kind_equality() {
423        assert_eq!(ErrorKind::Database, ErrorKind::Database);
424        assert_ne!(ErrorKind::Database, ErrorKind::Internal);
425    }
426
427    #[test]
428    fn test_error_kind_display() {
429        assert_eq!(format!("{}", ErrorKind::Database), "database error");
430        assert_eq!(format!("{}", ErrorKind::Internal), "internal error");
431        assert_eq!(format!("{}", ErrorKind::InvalidInput), "invalid input");
432    }
433
434    #[test]
435    fn test_error_kind_clone() {
436        let kind = ErrorKind::Database;
437        let cloned = kind;
438        assert_eq!(kind, cloned);
439    }
440
441    // ========================================================================
442    // Error constructor tests
443    // ========================================================================
444
445    #[test]
446    fn test_error_new() {
447        let err = Error::new(ErrorKind::Internal, "test error");
448        assert_eq!(err.kind(), ErrorKind::Internal);
449        assert_eq!(format!("{}", err), "test error");
450    }
451
452    #[test]
453    fn test_error_database() {
454        let err = Error::database("connection failed");
455        assert_eq!(err.kind(), ErrorKind::Database);
456        assert!(err.to_string().contains("connection failed"));
457    }
458
459    #[test]
460    fn test_error_database_config() {
461        let err = Error::database_config("invalid URL");
462        assert_eq!(err.kind(), ErrorKind::Database);
463        assert!(err.to_string().contains("Database configuration error"));
464        assert!(err.to_string().contains("invalid URL"));
465    }
466
467    #[test]
468    fn test_error_authentication() {
469        let err = Error::authentication("invalid token");
470        assert_eq!(err.kind(), ErrorKind::Authentication);
471        assert!(err.to_string().contains("invalid token"));
472    }
473
474    #[test]
475    fn test_error_config() {
476        let err = Error::config("missing field");
477        assert_eq!(err.kind(), ErrorKind::Configuration);
478        assert!(err.to_string().contains("missing field"));
479    }
480
481    #[test]
482    fn test_error_tls() {
483        let err = Error::tls("certificate expired");
484        assert_eq!(err.kind(), ErrorKind::Tls);
485        assert!(err.to_string().contains("certificate expired"));
486    }
487
488    #[test]
489    fn test_error_io() {
490        let err = Error::io("file not found");
491        assert_eq!(err.kind(), ErrorKind::Io);
492        assert!(err.to_string().contains("file not found"));
493    }
494
495    #[test]
496    fn test_error_invalid_input() {
497        let err = Error::invalid_input("bad request");
498        assert_eq!(err.kind(), ErrorKind::InvalidInput);
499        assert!(err.to_string().contains("bad request"));
500    }
501
502    #[test]
503    fn test_error_internal() {
504        let err = Error::internal("unexpected state");
505        assert_eq!(err.kind(), ErrorKind::Internal);
506        assert!(err.to_string().contains("unexpected state"));
507    }
508
509    #[cfg(feature = "circuit-breaker")]
510    #[test]
511    fn test_error_circuit_breaker_open() {
512        let err = Error::circuit_breaker_open("payment-api");
513        assert_eq!(err.kind(), ErrorKind::CircuitBreakerOpen);
514        assert!(err.to_string().contains("payment-api"));
515    }
516
517    #[cfg(feature = "circuit-breaker")]
518    #[test]
519    fn test_error_circuit_breaker_failed() {
520        let err = Error::circuit_breaker_failed("timeout");
521        assert_eq!(err.kind(), ErrorKind::CircuitBreakerFailed);
522        assert!(err.to_string().contains("timeout"));
523    }
524
525    // ========================================================================
526    // Error code tests
527    // ========================================================================
528
529    #[test]
530    fn test_error_code_database() {
531        let err = Error::database("test");
532        assert_eq!(err.error_code(), "DATABASE_ERROR");
533    }
534
535    #[test]
536    fn test_error_code_authentication() {
537        let err = Error::authentication("test");
538        assert_eq!(err.error_code(), "AUTH_ERROR");
539    }
540
541    #[test]
542    fn test_error_code_config() {
543        let err = Error::config("test");
544        assert_eq!(err.error_code(), "CONFIG_ERROR");
545    }
546
547    #[test]
548    fn test_error_code_tls() {
549        let err = Error::tls("test");
550        assert_eq!(err.error_code(), "TLS_ERROR");
551    }
552
553    #[test]
554    fn test_error_code_io() {
555        let err = Error::io("test");
556        assert_eq!(err.error_code(), "IO_ERROR");
557    }
558
559    #[test]
560    fn test_error_code_invalid_input() {
561        let err = Error::invalid_input("test");
562        assert_eq!(err.error_code(), "INVALID_INPUT");
563    }
564
565    #[test]
566    fn test_error_code_internal() {
567        let err = Error::internal("test");
568        assert_eq!(err.error_code(), "INTERNAL_ERROR");
569    }
570
571    #[cfg(feature = "circuit-breaker")]
572    #[test]
573    fn test_error_code_circuit_breaker_open() {
574        let err = Error::circuit_breaker_open("test");
575        assert_eq!(err.error_code(), "CIRCUIT_BREAKER_OPEN");
576    }
577
578    #[cfg(feature = "circuit-breaker")]
579    #[test]
580    fn test_error_code_circuit_breaker_failed() {
581        let err = Error::circuit_breaker_failed("test");
582        assert_eq!(err.error_code(), "CIRCUIT_BREAKER_CALL_FAILED");
583    }
584
585    // ========================================================================
586    // Status code tests
587    // ========================================================================
588
589    #[test]
590    fn test_status_code_database() {
591        let err = Error::database("test");
592        assert_eq!(err.status_code(), StatusCode::SERVICE_UNAVAILABLE);
593    }
594
595    #[test]
596    fn test_status_code_authentication() {
597        let err = Error::authentication("test");
598        assert_eq!(err.status_code(), StatusCode::UNAUTHORIZED);
599    }
600
601    #[test]
602    fn test_status_code_config() {
603        let err = Error::config("test");
604        assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
605    }
606
607    #[test]
608    fn test_status_code_tls() {
609        let err = Error::tls("test");
610        assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
611    }
612
613    #[test]
614    fn test_status_code_io() {
615        let err = Error::io("test");
616        assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
617    }
618
619    #[test]
620    fn test_status_code_invalid_input() {
621        let err = Error::invalid_input("test");
622        assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
623    }
624
625    #[test]
626    fn test_status_code_internal() {
627        let err = Error::internal("test");
628        assert_eq!(err.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
629    }
630
631    #[cfg(feature = "circuit-breaker")]
632    #[test]
633    fn test_status_code_circuit_breaker_open() {
634        let err = Error::circuit_breaker_open("test");
635        assert_eq!(err.status_code(), StatusCode::SERVICE_UNAVAILABLE);
636    }
637
638    #[cfg(feature = "circuit-breaker")]
639    #[test]
640    fn test_status_code_circuit_breaker_failed() {
641        let err = Error::circuit_breaker_failed("test");
642        assert_eq!(err.status_code(), StatusCode::BAD_GATEWAY);
643    }
644
645    // ========================================================================
646    // From trait tests
647    // ========================================================================
648
649    #[test]
650    fn test_from_io_error() {
651        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
652        let err: Error = io_err.into();
653        assert_eq!(err.kind(), ErrorKind::Io);
654    }
655
656    #[test]
657    fn test_from_toml_error() {
658        let toml_err = "invalid".parse::<toml::Value>().unwrap_err();
659        let err: Error = toml_err.into();
660        assert_eq!(err.kind(), ErrorKind::Configuration);
661    }
662
663    #[test]
664    fn test_from_url_parse_error() {
665        let url_err = url::Url::parse("not a url").unwrap_err();
666        let err: Error = url_err.into();
667        assert_eq!(err.kind(), ErrorKind::InvalidInput);
668    }
669
670    #[test]
671    fn test_from_var_error() {
672        let var_err = std::env::VarError::NotPresent;
673        let err: Error = var_err.into();
674        assert_eq!(err.kind(), ErrorKind::Configuration);
675    }
676
677    #[test]
678    fn test_from_invalid_header() {
679        let header_err = http::header::HeaderValue::from_bytes(b"\x00").unwrap_err();
680        let err: Error = header_err.into();
681        assert_eq!(err.kind(), ErrorKind::InvalidInput);
682    }
683
684    // ========================================================================
685    // ErrorResponse tests
686    // ========================================================================
687
688    #[test]
689    fn test_error_response_new() {
690        let response = ErrorResponse::new("TEST_CODE", "Test message");
691        assert_eq!(response.error_code, "TEST_CODE");
692        assert_eq!(response.message, "Test message");
693        assert!(response.details.is_none());
694    }
695
696    #[test]
697    fn test_error_response_with_details() {
698        let response = ErrorResponse::new("CODE", "message").with_details("extra info");
699        assert_eq!(response.error_code, "CODE");
700        assert_eq!(response.message, "message");
701        assert_eq!(response.details, Some("extra info".to_string()));
702    }
703
704    #[test]
705    fn test_to_error_response() {
706        let err = Error::internal("Something went wrong");
707        let response = err.to_error_response();
708        assert_eq!(response.error_code, "INTERNAL_ERROR");
709        assert!(response.message.contains("Something went wrong"));
710    }
711
712    // ========================================================================
713    // Misc trait tests
714    // ========================================================================
715
716    #[test]
717    fn test_error_debug() {
718        let err = Error::internal("test");
719        let debug_str = format!("{:?}", err);
720        assert!(debug_str.contains("Error"));
721        assert!(debug_str.contains("Internal"));
722    }
723
724    #[test]
725    fn test_error_display() {
726        let err = Error::internal("my error message");
727        assert_eq!(format!("{}", err), "my error message");
728    }
729
730    #[test]
731    fn test_error_into_inner() {
732        let err = Error::internal("test message");
733        let inner = err.into_inner();
734        assert_eq!(format!("{}", inner), "test message");
735    }
736
737    #[test]
738    fn test_error_source_trait() {
739        let err = Error::internal("test");
740        assert!(StdError::source(&err).is_some());
741    }
742}