1use axum::{
34 Json,
35 http::StatusCode,
36 response::{IntoResponse, Response},
37};
38use serde::Serialize;
39use std::fmt;
40use thiserror::Error;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
53#[non_exhaustive]
54pub enum ErrorKind {
55 #[error("database error")]
57 Database,
58
59 #[error("authentication error")]
61 Authentication,
62
63 #[error("configuration error")]
65 Configuration,
66
67 #[error("TLS error")]
69 Tls,
70
71 #[error("I/O error")]
73 Io,
74
75 #[error("invalid input")]
77 InvalidInput,
78
79 #[error("circuit breaker open")]
81 CircuitBreakerOpen,
82
83 #[error("circuit breaker call failed")]
85 CircuitBreakerFailed,
86
87 #[error("internal error")]
89 Internal,
90}
91
92pub struct Error {
119 kind: ErrorKind,
120 source: Box<dyn std::error::Error + Send + Sync + 'static>,
121}
122
123impl Error {
124 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 pub fn kind(&self) -> ErrorKind {
160 self.kind
161 }
162
163 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 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 pub fn to_error_response(&self) -> ErrorResponse {
197 ErrorResponse::new(self.error_code(), self.to_string())
198 }
199
200 pub fn into_inner(self) -> Box<dyn std::error::Error + Send + Sync + 'static> {
202 self.source
203 }
204}
205
206impl Error {
211 pub fn database(msg: impl Into<String>) -> Self {
213 Self::new(ErrorKind::Database, msg.into())
214 }
215
216 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 pub fn authentication(msg: impl Into<String>) -> Self {
229 Self::new(ErrorKind::Authentication, msg.into())
230 }
231
232 pub fn config(msg: impl Into<String>) -> Self {
234 Self::new(ErrorKind::Configuration, msg.into())
235 }
236
237 pub fn tls(msg: impl Into<String>) -> Self {
239 Self::new(ErrorKind::Tls, msg.into())
240 }
241
242 pub fn io(msg: impl Into<String>) -> Self {
244 Self::new(ErrorKind::Io, msg.into())
245 }
246
247 pub fn from_io(err: std::io::Error) -> Self {
249 Self::new(ErrorKind::Io, err)
250 }
251
252 pub fn invalid_input(msg: impl Into<String>) -> Self {
254 Self::new(ErrorKind::InvalidInput, msg.into())
255 }
256
257 #[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 #[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 pub fn internal(msg: impl Into<String>) -> Self {
274 Self::new(ErrorKind::Internal, msg.into())
275 }
276}
277
278impl 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
319impl 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#[derive(Debug, Serialize)]
380#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
381pub struct ErrorResponse {
382 pub error_code: String,
384 pub message: String,
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub details: Option<String>,
389}
390
391impl ErrorResponse {
392 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 pub fn with_details(mut self, details: impl Into<String>) -> Self {
403 self.details = Some(details.into());
404 self
405 }
406}
407
408#[cfg(test)]
413mod tests {
414 use super::*;
415 use std::error::Error as StdError;
416
417 #[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 #[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 #[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 #[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 #[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 #[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 #[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}