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}