apollo-errors 0.7.0

Structured error handling with automatic format conversion
Documentation
//! ErrorExt extension trait for dynamic error formatting

use crate::metadata::FormatConfig;
use crate::registry::{
    http_headers, http_status, render_graphql, render_html, render_json, render_jsonrpc,
    render_text,
};

/// Create a fallback JSON error for unregistered errors
fn fallback_json(message: impl std::fmt::Display) -> serde_json::Value {
    serde_json::json!({
        "error": "UNKNOWN_ERROR",
        "message": message.to_string()
    })
}

/// Create a fallback HTML error for unregistered errors
fn fallback_html(message: impl std::fmt::Display) -> String {
    format!(
        "<div class=\"error\">\n<h3 class=\"error-code\">UNKNOWN_ERROR</h3>\n<p class=\"error-message\">{message}</p>\n</div>"
    )
}

/// Create a fallback GraphQL error for unregistered errors
fn fallback_graphql(message: impl std::fmt::Display) -> serde_json::Value {
    serde_json::json!({
        "message": message.to_string(),
        "extensions": {
            "code": "UNKNOWN_ERROR"
        }
    })
}

/// Create a fallback text error for unregistered errors
fn fallback_text(message: impl std::fmt::Display) -> String {
    format!("[UNKNOWN_ERROR] {message}")
}

/// Create a fallback JSON-RPC error for unregistered errors
fn fallback_jsonrpc(message: impl std::fmt::Display) -> serde_json::Value {
    serde_json::json!({
        "code": crate::private::DEFAULT_JSONRPC_CODE,
        "message": message.to_string(),
        "data": {
            "diagnostic_code": "UNKNOWN_ERROR"
        }
    })
}

/// Extension trait for formatting any error type
///
/// This trait is implemented for `dyn std::error::Error` and uses the
/// error registry to dynamically dispatch to the concrete error type's
/// `apollo_errors::Error` implementation.
///
/// # Example
///
/// ```ignore
/// use apollo_errors::{ErrorExt, FormatConfig};
///
/// fn handle_error(error: &dyn std::error::Error) {
///     let config = FormatConfig::default();
///     let graphql = error.to_graphql(config);
///     let json = error.to_json(config);
/// }
/// ```
pub trait ErrorExt {
    /// Render this error as JSON format
    ///
    /// # Errors
    ///
    /// Returns an error if any field fails to serialize to JSON
    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;

    /// Render this error as HTML
    fn to_html(&self, config: FormatConfig) -> String;

    /// Render this error as GraphQL JSON format
    ///
    /// # Errors
    ///
    /// Returns an error if any field fails to serialize to JSON
    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;

    /// Render this error as plain text
    fn to_text(&self, config: FormatConfig) -> String;

    /// Render this error in debug format using Rust's standard `Debug` output.
    fn to_debug(&self) -> String
    where
        Self: std::fmt::Debug,
    {
        format!("{self:?}")
    }

    /// Render this error as JSON-RPC 2.0 error format
    ///
    /// # Errors
    ///
    /// Returns an error if any field fails to serialize to JSON
    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;

    /// Get the HTTP status code for this error
    fn http_status(&self) -> http::StatusCode;

    /// Get HTTP headers for this error
    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)>;
}

/// Blanket implementation for any concrete error type
impl<E: std::error::Error + 'static> ErrorExt for E {
    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
        render_json(self, config).unwrap_or_else(|| Ok(fallback_json(self)))
    }

    fn to_html(&self, config: FormatConfig) -> String {
        render_html(self, config).unwrap_or_else(|| fallback_html(self))
    }

    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
        render_graphql(self, config).unwrap_or_else(|| Ok(fallback_graphql(self)))
    }

    fn to_text(&self, config: FormatConfig) -> String {
        render_text(self, config).unwrap_or_else(|| fallback_text(self))
    }

    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
        render_jsonrpc(self, config).unwrap_or_else(|| Ok(fallback_jsonrpc(self)))
    }

    fn http_status(&self) -> http::StatusCode {
        http_status(self).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR)
    }

    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)> {
        http_headers(self).unwrap_or_default()
    }
}

/// Extension trait for heap-allocated error types without 'static bounds
///
/// This trait provides error formatting for heap-allocated errors like
/// `Box<dyn Error + Send + Sync>` and `Arc<dyn Error + Send + Sync>` that
/// don't have `'static` bounds. These types are commonly used in async contexts
/// (e.g., tower::BoxError) where the error lifetime isn't statically known.
///
/// # Why This Trait Exists
///
/// The regular `ErrorExt` trait requires `'static` bounds because it uses
/// the registry's TypeId-based dispatch. However, types like `Box<dyn Error + Send + Sync>`
/// cannot satisfy the `'static` requirement. This trait provides an alternative
/// path that:
/// 1. Unwraps the heap-allocated error to get `&dyn Error`
/// 2. Attempts nested wrapper unwrapping (e.g., `Box<Arc<T>>`)
/// 3. Tries registry lookup on the inner error
/// 4. Falls back to generic error rendering if no match is found
///
/// # Example
///
/// ```ignore
/// use apollo_errors::{HeapErrorExt, FormatConfig};
/// use tower::BoxError;
///
/// fn handle_boxed_error(error: BoxError) {
///     let config = FormatConfig::default();
///     let json = error.to_json(config);
///     let text = error.to_text(config);
/// }
/// ```
pub trait HeapErrorExt {
    /// Render this heap-allocated error as JSON format
    ///
    /// # Errors
    ///
    /// Returns an error if any field fails to serialize to JSON
    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;

    /// Render this heap-allocated error as HTML
    fn to_html(&self, config: FormatConfig) -> String;

    /// Render this heap-allocated error as GraphQL JSON format
    ///
    /// # Errors
    ///
    /// Returns an error if any field fails to serialize to JSON
    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;

    /// Render this heap-allocated error as plain text
    fn to_text(&self, config: FormatConfig) -> String;

    /// Render this heap-allocated error in debug format using Rust's standard `Debug` output.
    fn to_debug(&self) -> String;

    /// Render this heap-allocated error as JSON-RPC 2.0 error format
    ///
    /// # Errors
    ///
    /// Returns an error if any field fails to serialize to JSON
    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error>;

    /// Get the HTTP status code for this heap-allocated error
    fn http_status(&self) -> http::StatusCode;

    /// Get HTTP headers for this heap-allocated error
    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)>;
}

/// Implementation for Box<dyn Error + Send + Sync>
///
/// This is the most common case for async error handling (e.g., tower::BoxError).
/// The implementation unwraps the Box, checks for nested Arc wrappers, and attempts
/// registry lookup before falling back to generic error formatting.
impl HeapErrorExt for Box<dyn std::error::Error + Send + Sync> {
    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
        let error_ref: &dyn std::error::Error = self.as_ref();

        // Try to unwrap nested Arc wrapper
        if let Some(nested_arc) =
            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
        {
            return HeapErrorExt::to_json(nested_arc, config);
        }

        render_json(error_ref, config).unwrap_or_else(|| Ok(fallback_json(error_ref)))
    }

    fn to_html(&self, config: FormatConfig) -> String {
        let error_ref: &dyn std::error::Error = self.as_ref();

        // Try to unwrap nested Arc wrapper
        if let Some(nested_arc) =
            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
        {
            return HeapErrorExt::to_html(nested_arc, config);
        }

        render_html(error_ref, config).unwrap_or_else(|| fallback_html(error_ref))
    }

    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
        let error_ref: &dyn std::error::Error = self.as_ref();

        // Try to unwrap nested Arc wrapper
        if let Some(nested_arc) =
            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
        {
            return HeapErrorExt::to_graphql(nested_arc, config);
        }

        render_graphql(error_ref, config).unwrap_or_else(|| Ok(fallback_graphql(error_ref)))
    }

    fn to_text(&self, config: FormatConfig) -> String {
        let error_ref: &dyn std::error::Error = self.as_ref();

        // Try to unwrap nested Arc wrapper
        if let Some(nested_arc) =
            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
        {
            return HeapErrorExt::to_text(nested_arc, config);
        }

        render_text(error_ref, config).unwrap_or_else(|| fallback_text(error_ref))
    }

    fn to_debug(&self) -> String {
        format!("{self:?}")
    }

    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
        let error_ref: &dyn std::error::Error = self.as_ref();

        // Try to unwrap nested Arc wrapper
        if let Some(nested_arc) =
            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
        {
            return HeapErrorExt::to_jsonrpc(nested_arc, config);
        }

        render_jsonrpc(error_ref, config).unwrap_or_else(|| Ok(fallback_jsonrpc(error_ref)))
    }

    fn http_status(&self) -> http::StatusCode {
        let error_ref: &dyn std::error::Error = self.as_ref();

        // Try to unwrap nested Arc wrapper
        if let Some(nested_arc) =
            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
        {
            return HeapErrorExt::http_status(nested_arc);
        }

        http_status(error_ref).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR)
    }

    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)> {
        let error_ref: &dyn std::error::Error = self.as_ref();

        // Try to unwrap nested Arc wrapper
        if let Some(nested_arc) =
            error_ref.downcast_ref::<std::sync::Arc<dyn std::error::Error + Send + Sync>>()
        {
            return HeapErrorExt::http_headers(nested_arc);
        }

        http_headers(error_ref).unwrap_or_default()
    }
}

/// Implementation for Arc<dyn Error + Send + Sync>
///
/// Similar to the Box implementation but for reference-counted errors.
/// This is less common but can occur in scenarios where errors need to be
/// shared across multiple tasks or threads.
impl HeapErrorExt for std::sync::Arc<dyn std::error::Error + Send + Sync> {
    fn to_json(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
        let error_ref: &dyn std::error::Error = self.as_ref();
        render_json(error_ref, config).unwrap_or_else(|| Ok(fallback_json(error_ref)))
    }

    fn to_html(&self, config: FormatConfig) -> String {
        let error_ref: &dyn std::error::Error = self.as_ref();
        render_html(error_ref, config).unwrap_or_else(|| fallback_html(error_ref))
    }

    fn to_graphql(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
        let error_ref: &dyn std::error::Error = self.as_ref();
        render_graphql(error_ref, config).unwrap_or_else(|| Ok(fallback_graphql(error_ref)))
    }

    fn to_text(&self, config: FormatConfig) -> String {
        let error_ref: &dyn std::error::Error = self.as_ref();
        render_text(error_ref, config).unwrap_or_else(|| fallback_text(error_ref))
    }

    fn to_debug(&self) -> String {
        format!("{self:?}")
    }

    fn to_jsonrpc(&self, config: FormatConfig) -> Result<serde_json::Value, serde_json::Error> {
        let error_ref: &dyn std::error::Error = self.as_ref();
        render_jsonrpc(error_ref, config).unwrap_or_else(|| Ok(fallback_jsonrpc(error_ref)))
    }

    fn http_status(&self) -> http::StatusCode {
        let error_ref: &dyn std::error::Error = self.as_ref();
        http_status(error_ref).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR)
    }

    fn http_headers(&self) -> Vec<(http::HeaderName, http::HeaderValue)> {
        let error_ref: &dyn std::error::Error = self.as_ref();
        http_headers(error_ref).unwrap_or_default()
    }
}