Skip to main content

apollo_errors/
ext.rs

1//! ErrorExt extension trait for dynamic error formatting
2
3use crate::metadata::FormatConfig;
4use crate::registry::{
5    http_headers, http_status, render_graphql, render_html, render_json, render_jsonrpc,
6    render_text,
7};
8
9/// Create a fallback JSON error for unregistered errors
10fn fallback_json(message: impl std::fmt::Display) -> serde_json::Value {
11    serde_json::json!({
12        "error": "UNKNOWN_ERROR",
13        "message": message.to_string()
14    })
15}
16
17/// Create a fallback HTML error for unregistered errors
18fn fallback_html(message: impl std::fmt::Display) -> String {
19    format!(
20        "<div class=\"error\">\n<h3 class=\"error-code\">UNKNOWN_ERROR</h3>\n<p class=\"error-message\">{message}</p>\n</div>"
21    )
22}
23
24/// Create a fallback GraphQL error for unregistered errors
25fn fallback_graphql(message: impl std::fmt::Display) -> serde_json::Value {
26    serde_json::json!({
27        "message": message.to_string(),
28        "extensions": {
29            "code": "UNKNOWN_ERROR"
30        }
31    })
32}
33
34/// Create a fallback text error for unregistered errors
35fn fallback_text(message: impl std::fmt::Display) -> String {
36    format!("[UNKNOWN_ERROR] {message}")
37}
38
39/// Create a fallback JSON-RPC error for unregistered errors
40fn fallback_jsonrpc(message: impl std::fmt::Display) -> serde_json::Value {
41    serde_json::json!({
42        "code": crate::private::DEFAULT_JSONRPC_CODE,
43        "message": message.to_string(),
44        "data": {
45            "diagnostic_code": "UNKNOWN_ERROR"
46        }
47    })
48}
49
50/// Extension trait for formatting any error type
51///
52/// This trait is implemented for `dyn std::error::Error` and uses the
53/// error registry to dynamically dispatch to the concrete error type's
54/// `apollo_errors::Error` implementation.
55///
56/// # Example
57///
58/// ```ignore
59/// use apollo_errors::{ErrorExt, FormatConfig};
60///
61/// fn handle_error(error: &dyn std::error::Error) {
62///     let config = FormatConfig::default();
63///     let graphql = error.to_graphql(config);
64///     let json = error.to_json(config);
65/// }
66/// ```
67pub trait ErrorExt {
68    /// Render this error as JSON format
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if any field fails to serialize to JSON
73    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;
74
75    /// Render this error as HTML
76    fn to_html(&self, config: FormatConfig) -> String;
77
78    /// Render this error as GraphQL JSON format
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if any field fails to serialize to JSON
83    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;
84
85    /// Render this error as plain text
86    fn to_text(&self, config: FormatConfig) -> String;
87
88    /// Render this error in debug format using Rust's standard `Debug` output.
89    fn to_debug(&self) -> String
90    where
91        Self: std::fmt::Debug,
92    {
93        format!("{self:?}")
94    }
95
96    /// Render this error as JSON-RPC 2.0 error format
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if any field fails to serialize to JSON
101    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;
102
103    /// Get the HTTP status code for this error
104    fn http_status(&self) -> http::StatusCode;
105
106    /// Get HTTP headers for this error
107    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)>;
108}
109
110/// Blanket implementation for any concrete error type
111impl<E: std::error::Error + 'static> ErrorExt for E {
112    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
113        render_json(self, config).unwrap_or_else(|| Ok(fallback_json(self)))
114    }
115
116    fn to_html(&self, config: FormatConfig) -> String {
117        render_html(self, config).unwrap_or_else(|| fallback_html(self))
118    }
119
120    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
121        render_graphql(self, config).unwrap_or_else(|| Ok(fallback_graphql(self)))
122    }
123
124    fn to_text(&self, config: FormatConfig) -> String {
125        render_text(self, config).unwrap_or_else(|| fallback_text(self))
126    }
127
128    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
129        render_jsonrpc(self, config).unwrap_or_else(|| Ok(fallback_jsonrpc(self)))
130    }
131
132    fn http_status(&self) -> http::StatusCode {
133        http_status(self).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR)
134    }
135
136    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)> {
137        http_headers(self).unwrap_or_default()
138    }
139}
140
141/// Extension trait for heap-allocated error types without 'static bounds
142///
143/// This trait provides error formatting for heap-allocated errors like
144/// `Box<dyn Error + Send + Sync>` and `Arc<dyn Error + Send + Sync>` that
145/// don't have `'static` bounds. These types are commonly used in async contexts
146/// (e.g., tower::BoxError) where the error lifetime isn't statically known.
147///
148/// # Why This Trait Exists
149///
150/// The regular `ErrorExt` trait requires `'static` bounds because it uses
151/// the registry's TypeId-based dispatch. However, types like `Box<dyn Error + Send + Sync>`
152/// cannot satisfy the `'static` requirement. This trait provides an alternative
153/// path that:
154/// 1. Unwraps the heap-allocated error to get `&dyn Error`
155/// 2. Attempts nested wrapper unwrapping (e.g., `Box<Arc<T>>`)
156/// 3. Tries registry lookup on the inner error
157/// 4. Falls back to generic error rendering if no match is found
158///
159/// # Example
160///
161/// ```ignore
162/// use apollo_errors::{HeapErrorExt, FormatConfig};
163/// use tower::BoxError;
164///
165/// fn handle_boxed_error(error: BoxError) {
166///     let config = FormatConfig::default();
167///     let json = error.to_json(config);
168///     let text = error.to_text(config);
169/// }
170/// ```
171pub trait HeapErrorExt {
172    /// Render this heap-allocated error as JSON format
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if any field fails to serialize to JSON
177    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;
178
179    /// Render this heap-allocated error as HTML
180    fn to_html(&self, config: FormatConfig) -> String;
181
182    /// Render this heap-allocated error as GraphQL JSON format
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if any field fails to serialize to JSON
187    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;
188
189    /// Render this heap-allocated error as plain text
190    fn to_text(&self, config: FormatConfig) -> String;
191
192    /// Render this heap-allocated error in debug format using Rust's standard `Debug` output.
193    fn to_debug(&self) -> String;
194
195    /// Render this heap-allocated error as JSON-RPC 2.0 error format
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if any field fails to serialize to JSON
200    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;
201
202    /// Get the HTTP status code for this heap-allocated error
203    fn http_status(&self) -> http::StatusCode;
204
205    /// Get HTTP headers for this heap-allocated error
206    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)>;
207}
208
209/// Implementation for Box<dyn Error + Send + Sync>
210///
211/// This is the most common case for async error handling (e.g., tower::BoxError).
212/// The implementation unwraps the Box, checks for nested Arc wrappers, and attempts
213/// registry lookup before falling back to generic error formatting.
214impl HeapErrorExt for Box<dyn std::error::Error + Send + Sync> {
215    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
216        let error_ref: &dyn std::error::Error = self.as_ref();
217
218        // Try to unwrap nested Arc wrapper
219        if let Some(nested_arc) =
220            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
221        {
222            return HeapErrorExt::to_json(nested_arc, config);
223        }
224
225        render_json(error_ref, config).unwrap_or_else(|| Ok(fallback_json(error_ref)))
226    }
227
228    fn to_html(&self, config: FormatConfig) -> String {
229        let error_ref: &dyn std::error::Error = self.as_ref();
230
231        // Try to unwrap nested Arc wrapper
232        if let Some(nested_arc) =
233            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
234        {
235            return HeapErrorExt::to_html(nested_arc, config);
236        }
237
238        render_html(error_ref, config).unwrap_or_else(|| fallback_html(error_ref))
239    }
240
241    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
242        let error_ref: &dyn std::error::Error = self.as_ref();
243
244        // Try to unwrap nested Arc wrapper
245        if let Some(nested_arc) =
246            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
247        {
248            return HeapErrorExt::to_graphql(nested_arc, config);
249        }
250
251        render_graphql(error_ref, config).unwrap_or_else(|| Ok(fallback_graphql(error_ref)))
252    }
253
254    fn to_text(&self, config: FormatConfig) -> String {
255        let error_ref: &dyn std::error::Error = self.as_ref();
256
257        // Try to unwrap nested Arc wrapper
258        if let Some(nested_arc) =
259            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
260        {
261            return HeapErrorExt::to_text(nested_arc, config);
262        }
263
264        render_text(error_ref, config).unwrap_or_else(|| fallback_text(error_ref))
265    }
266
267    fn to_debug(&self) -> String {
268        format!("{self:?}")
269    }
270
271    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
272        let error_ref: &dyn std::error::Error = self.as_ref();
273
274        // Try to unwrap nested Arc wrapper
275        if let Some(nested_arc) =
276            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
277        {
278            return HeapErrorExt::to_jsonrpc(nested_arc, config);
279        }
280
281        render_jsonrpc(error_ref, config).unwrap_or_else(|| Ok(fallback_jsonrpc(error_ref)))
282    }
283
284    fn http_status(&self) -> http::StatusCode {
285        let error_ref: &dyn std::error::Error = self.as_ref();
286
287        // Try to unwrap nested Arc wrapper
288        if let Some(nested_arc) =
289            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
290        {
291            return HeapErrorExt::http_status(nested_arc);
292        }
293
294        http_status(error_ref).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR)
295    }
296
297    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)> {
298        let error_ref: &dyn std::error::Error = self.as_ref();
299
300        // Try to unwrap nested Arc wrapper
301        if let Some(nested_arc) =
302            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
303        {
304            return HeapErrorExt::http_headers(nested_arc);
305        }
306
307        http_headers(error_ref).unwrap_or_default()
308    }
309}
310
311/// Implementation for Arc<dyn Error + Send + Sync>
312///
313/// Similar to the Box implementation but for reference-counted errors.
314/// This is less common but can occur in scenarios where errors need to be
315/// shared across multiple tasks or threads.
316impl HeapErrorExt for std::sync::Arc<dyn std::error::Error + Send + Sync> {
317    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
318        let error_ref: &dyn std::error::Error = self.as_ref();
319        render_json(error_ref, config).unwrap_or_else(|| Ok(fallback_json(error_ref)))
320    }
321
322    fn to_html(&self, config: FormatConfig) -> String {
323        let error_ref: &dyn std::error::Error = self.as_ref();
324        render_html(error_ref, config).unwrap_or_else(|| fallback_html(error_ref))
325    }
326
327    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
328        let error_ref: &dyn std::error::Error = self.as_ref();
329        render_graphql(error_ref, config).unwrap_or_else(|| Ok(fallback_graphql(error_ref)))
330    }
331
332    fn to_text(&self, config: FormatConfig) -> String {
333        let error_ref: &dyn std::error::Error = self.as_ref();
334        render_text(error_ref, config).unwrap_or_else(|| fallback_text(error_ref))
335    }
336
337    fn to_debug(&self) -> String {
338        format!("{self:?}")
339    }
340
341    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
342        let error_ref: &dyn std::error::Error = self.as_ref();
343        render_jsonrpc(error_ref, config).unwrap_or_else(|| Ok(fallback_jsonrpc(error_ref)))
344    }
345
346    fn http_status(&self) -> http::StatusCode {
347        let error_ref: &dyn std::error::Error = self.as_ref();
348        http_status(error_ref).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR)
349    }
350
351    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)> {
352        let error_ref: &dyn std::error::Error = self.as_ref();
353        http_headers(error_ref).unwrap_or_default()
354    }
355}