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}