Skip to main content

apollo_errors/
lib.rs

1//! Structured error handling with multi-format output.
2//!
3//! Define errors once and render them to JSON, GraphQL, HTML, or plain text.
4//!
5//! # Quick Start
6//!
7//! ```rust
8//! use apollo_errors::Error;
9//! use miette::Diagnostic;
10//!
11//! #[derive(Debug, Error, Diagnostic)]
12//! pub enum AuthError {
13//!     #[error("Invalid credentials for user {username}")]
14//!     #[diagnostic(code(auth::invalid_credentials))]
15//!     InvalidCredentials {
16//!         #[extension]
17//!         username: String,
18//!     },
19//! }
20//!
21//! let error = AuthError::InvalidCredentials {
22//!     username: "alice".to_string(),
23//! };
24//!
25//! // Render to different formats
26//! let json = error.to_json().unwrap();
27//! let graphql = error.to_graphql().unwrap();
28//! let jsonrpc = error.to_jsonrpc().unwrap();
29//! let html = error.to_html();
30//! let text = error.to_text();
31//! ```
32//!
33//! # Defining Errors
34//!
35//! Errors require three derives: `Debug`, `Error`, and `Diagnostic`.
36//!
37//! ```rust,ignore
38//! #[derive(Debug, Error, Diagnostic)]
39//! pub enum MyError {
40//!     #[error("Something went wrong")]
41//!     #[diagnostic(code(service::something_wrong))]
42//!     SomethingWrong,
43//! }
44//! ```
45//!
46//! # Attributes
47//!
48//! ## Error Message
49//!
50//! Use `#[error("...")]` to define the error message. Field interpolation is supported:
51//!
52//! ```rust,ignore
53//! #[error("Failed to connect to {host}:{port}")]
54//! ConnectionFailed { host: String, port: u16 },
55//! ```
56//!
57//! ## Error Code
58//!
59//! Use `#[diagnostic(code(...))]` to define a unique error code. Codes must have
60//! at least two `::` separated segments, all lowercase:
61//!
62//! ```rust,ignore
63//! #[diagnostic(code(db::connection_failed))]
64//! #[diagnostic(code(auth::invalid_token))]
65//! ```
66//!
67//! ## Extension Fields
68//!
69//! Mark fields with `#[extension]` to include them in JSON and GraphQL output:
70//!
71//! ```rust,ignore
72//! InvalidPort {
73//!     #[extension]
74//!     port: u16,
75//!     #[extension]
76//!     config_file: String,
77//! },
78//! ```
79//!
80//! ## HTTP Status
81//!
82//! Specify an HTTP status code (defaults to 500):
83//!
84//! ```rust,ignore
85//! #[http_status(404)]
86//! NotFound,
87//! ```
88//!
89//! ## HTTP Headers
90//!
91//! Mark fields to be returned as HTTP response headers. Supported types are `u16`, `u32`,
92//! `u64`, `i16`, `i32`, `i64`, `bool`, and `HeaderValue`:
93//!
94//! ```rust,ignore
95//! #[http_status(429)]
96//! RateLimitExceeded {
97//!     #[http_header("Retry-After")]
98//!     retry_after: u64,
99//!     #[http_header("X-RateLimit-Remaining")]
100//!     remaining: u32,
101//! },
102//! ```
103//!
104//! Header names are validated at compile time against RFC 7230. Headers are
105//! automatically set when using [`tower_http::ErrorLayer`]. For `Option<T>` fields,
106//! the header is only included when the value is `Some`.
107//!
108//! ## JSON-RPC Code
109//!
110//! Specify a JSON-RPC 2.0 error code (defaults to -32000, "Server error"):
111//!
112//! ```rust,ignore
113//! #[jsonrpc_code(-32602)]
114//! InvalidParams { param: String },
115//! ```
116//!
117//! Reserved JSON-RPC codes:
118//! - `-32700`: Parse error
119//! - `-32600`: Invalid Request
120//! - `-32601`: Method not found
121//! - `-32602`: Invalid params
122//! - `-32603`: Internal error
123//! - `-32000` to `-32099`: Server error (available for application use)
124//!
125//! ## Help Text and URLs
126//!
127//! Provide additional context for users:
128//!
129//! ```rust,ignore
130//! #[diagnostic(
131//!     code(config::invalid),
132//!     help("Check your configuration file"),
133//!     url("https://docs.example.com/errors/config-invalid")
134//! )]
135//! ```
136//!
137//! ## Error Chaining
138//!
139//! Use `#[source]` to chain errors, or `#[from]` to also generate a `From` impl:
140//!
141//! ```rust,ignore
142//! #[error("Database operation failed")]
143//! #[diagnostic(code(db::operation_failed))]
144//! DatabaseError {
145//!     #[from]
146//!     source: std::io::Error,
147//! },
148//! ```
149//!
150//! # Dynamic Dispatch
151//!
152//! Format any `std::error::Error` using the extension traits:
153//!
154//! ```rust
155//! use apollo_errors::{ErrorExt, HeapErrorExt};
156//!
157//! fn handle_error(error: Box<dyn std::error::Error + Send + Sync>) {
158//!     let json = error.to_json().unwrap();
159//!     println!("{}", json);
160//! }
161//! ```
162
163// Re-export the derive macro
164pub use apollo_errors_derive::Error;
165
166// Re-export miette for user convenience
167pub use miette;
168
169// Re-export http crate for Error trait return types
170pub use http;
171
172mod catalog;
173mod error;
174mod ext;
175mod html;
176mod metadata;
177mod registry;
178
179pub use catalog::CatalogErrorEntry as ErrorMetadata;
180pub use catalog::CatalogVariantEntry as ErrorVariantMetadata;
181pub use catalog::error_catalog;
182pub use error::Error;
183pub use ext::{ErrorExt, HeapErrorExt};
184pub use metadata::FieldMetadata as ErrorFieldMetadata;
185
186#[cfg(feature = "tower")]
187pub mod tower_http;
188
189#[doc(hidden)]
190pub mod private {
191    pub use crate::error::{diagnostic_code, diagnostic_help};
192    pub use crate::html::HtmlEscaped;
193    pub use crate::metadata::*;
194    pub use crate::registry::*;
195    pub use linkme;
196    pub use serde_json;
197
198    /// Default JSON-RPC error code when not specified (-32000 = "Server error")
199    pub const DEFAULT_JSONRPC_CODE: i32 = -32000;
200
201    // ToHeaderValue trait for http_header attribute support
202    use http::HeaderValue;
203
204    pub trait ToHeaderValue {
205        fn to_header_value(&self) -> Option<HeaderValue>;
206    }
207
208    impl ToHeaderValue for HeaderValue {
209        fn to_header_value(&self) -> Option<HeaderValue> {
210            Some(self.clone())
211        }
212    }
213
214    impl ToHeaderValue for bool {
215        fn to_header_value(&self) -> Option<HeaderValue> {
216            Some(HeaderValue::from_static(if *self {
217                "true"
218            } else {
219                "false"
220            }))
221        }
222    }
223
224    macro_rules! impl_to_header_value_for_int {
225        ($($ty:ty),*) => {
226            $(
227                impl ToHeaderValue for $ty {
228                    fn to_header_value(&self) -> Option<HeaderValue> {
229                        Some(HeaderValue::from(*self))
230                    }
231                }
232            )*
233        };
234    }
235
236    impl_to_header_value_for_int!(u16, u32, u64, i16, i32, i64);
237
238    /// Converts a source error reference to `&dyn Error + 'static`.
239    ///
240    /// This trait exists to handle `Box<dyn Error + Send + Sync>` in `#[source]` fields
241    /// without requiring the blanket `impl<E: Error> Error for Box<E>` (which requires
242    /// `E: Sized` and fails for `dyn Error + Send + Sync`). Method-call auto-deref walks
243    /// through `Box<T>` to `T = dyn Error + Send + Sync`, where the specific impls below apply.
244    pub trait AsDynError<'a> {
245        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a);
246    }
247
248    impl<'a, T: ::std::error::Error + 'a> AsDynError<'a> for T {
249        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a) {
250            self
251        }
252    }
253
254    impl<'a> AsDynError<'a> for dyn ::std::error::Error + 'a {
255        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a) {
256            self
257        }
258    }
259
260    impl<'a> AsDynError<'a> for dyn ::std::error::Error + Send + 'a {
261        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a) {
262            self
263        }
264    }
265
266    impl<'a> AsDynError<'a> for dyn ::std::error::Error + Send + Sync + 'a {
267        fn as_dyn_error(&self) -> &(dyn ::std::error::Error + 'a) {
268            self
269        }
270    }
271}