Skip to main content

guardrail_core/
error.rs

1//! Unified error type for all guardrail-rs crates.
2//!
3//! ## A note on `Upstream` and the dependency budget
4//!
5//! Per the project's dependency budget (spec §20), `guardrail-core` does not
6//! depend on `reqwest` by default — it is the foundational crate and should
7//! remain usable in constrained / embedded contexts without pulling in an
8//! HTTP client. However, the spec's error model calls for
9//! `Upstream(#[from] reqwest::Error)` so that `?` works ergonomically at the
10//! call site in `guardrail-proxy`.
11//!
12//! We resolve this by making `reqwest` an **optional** dependency gated
13//! behind the `reqwest-errors` feature. `guardrail-proxy` enables this
14//! feature; consumers of `guardrail-core` alone (e.g. a future WASM build,
15//! or someone embedding just the classifiers) get the lighter
16//! `Upstream(String)` variant with no behavioral difference apart from the
17//! `#[from]` conversion.
18
19/// Top-level error type used throughout `guardrail-rs`.
20///
21/// All errors are variants of this enum so that callers can handle them
22/// uniformly regardless of which crate or stage produced them.
23#[derive(Debug, thiserror::Error)]
24pub enum GuardrailError {
25    /// A pipeline stage failed during evaluation.
26    #[error("Pipeline stage '{stage}' failed: {source}")]
27    StageFailed {
28        /// The name of the stage that failed.
29        stage: String,
30        /// The underlying error.
31        #[source]
32        source: Box<dyn std::error::Error + Send + Sync>,
33    },
34
35    /// A configuration value was invalid or missing.
36    #[error("Configuration error: {0}")]
37    Config(String),
38
39    /// The upstream LLM request failed (`reqwest-errors` feature enabled).
40    ///
41    /// Carries the original [`reqwest::Error`] for inspection (status code,
42    /// timeout vs. connect-error classification, etc.) via `?`.
43    #[cfg(feature = "reqwest-errors")]
44    #[error("Upstream request failed: {0}")]
45    Upstream(#[from] reqwest::Error),
46
47    /// The upstream LLM request failed (`reqwest-errors` feature disabled).
48    ///
49    /// Carries a pre-formatted message instead of the original error type,
50    /// avoiding a hard dependency on `reqwest` in minimal builds.
51    #[cfg(not(feature = "reqwest-errors"))]
52    #[error("Upstream request failed: {0}")]
53    Upstream(String),
54
55    /// JSON serialization or deserialization failed.
56    #[error("Serialization error: {0}")]
57    Serialization(#[from] serde_json::Error),
58
59    /// An I/O error occurred.
60    #[error("I/O error: {0}")]
61    Io(#[from] std::io::Error),
62
63    /// A regex pattern failed to compile.
64    #[error("Regex error: {0}")]
65    Regex(#[from] regex::Error),
66
67    /// An internal invariant was violated (should never happen in production).
68    #[error("Internal error: {0}")]
69    Internal(String),
70}
71
72impl GuardrailError {
73    /// Wrap any error as a `StageFailed` variant.
74    ///
75    /// # Examples
76    ///
77    /// ```rust
78    /// use guardrail_core::GuardrailError;
79    ///
80    /// let err = GuardrailError::stage_failed(
81    ///     "my_stage",
82    ///     std::io::Error::new(std::io::ErrorKind::Other, "disk full"),
83    /// );
84    /// assert!(err.to_string().contains("my_stage"));
85    /// ```
86    pub fn stage_failed(
87        stage: impl Into<String>,
88        source: impl std::error::Error + Send + Sync + 'static,
89    ) -> Self {
90        GuardrailError::StageFailed {
91            stage: stage.into(),
92            source: Box::new(source),
93        }
94    }
95
96    /// Construct an `Upstream` error from a message string.
97    ///
98    /// Available regardless of the `reqwest-errors` feature, so call sites
99    /// that just want to report a string don't need to feature-gate.
100    ///
101    /// # Examples
102    ///
103    /// ```rust
104    /// use guardrail_core::GuardrailError;
105    ///
106    /// let err = GuardrailError::upstream("connection refused");
107    /// assert!(err.to_string().contains("connection refused"));
108    /// ```
109    pub fn upstream(message: impl Into<String>) -> Self {
110        #[cfg(feature = "reqwest-errors")]
111        {
112            // We can't construct a real reqwest::Error from a string, so in
113            // this feature configuration we fall back to Internal for
114            // string-only construction. Call sites with an actual
115            // reqwest::Error should use `?` / `.into()` instead.
116            GuardrailError::Internal(format!("upstream: {}", message.into()))
117        }
118        #[cfg(not(feature = "reqwest-errors"))]
119        {
120            GuardrailError::Upstream(message.into())
121        }
122    }
123}