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}