Skip to main content

apollo_errors/
ext.rs

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