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