graphql_starter/error/
graphql.rs

1use std::{any::Any, sync::Arc};
2
3use async_graphql::{ErrorExtensions, Name};
4use error_info::ErrorInfo;
5use indexmap::IndexMap;
6use tracing_error::SpanTrace;
7
8use super::{Error, GenericErrorCode};
9
10/// GraphQL Result that represents either success ([`Ok`]) or failure ([`Err`])
11pub type GraphQLResult<T, E = Box<GraphQLError>> = std::result::Result<T, E>;
12
13/// GraphQL error
14#[derive(Clone)]
15pub enum GraphQLError {
16    Async(async_graphql::Error, SpanTrace),
17    Custom(Box<Error>),
18}
19
20impl GraphQLError {
21    /// Creates a new [GraphQLError]
22    pub fn new(info: impl ErrorInfo + Send + Sync + 'static) -> Box<Self> {
23        Box::new(Self::Custom(Error::new(info)))
24    }
25
26    /// Creates a new internal server error
27    pub fn internal(reason: impl Into<String>) -> Box<Self> {
28        Box::new(Self::Custom(Error::internal(reason)))
29    }
30
31    /// Creates a new [GraphQLError]
32    pub fn from_err(error: Box<Error>) -> Box<Self> {
33        Box::new(Self::Custom(error))
34    }
35
36    /// Appends a property to the error
37    #[allow(clippy::boxed_local)]
38    pub fn with_property(self: Box<Self>, key: &str, value: serde_json::Value) -> Box<Self> {
39        match *self {
40            Self::Async(err, ctx) => {
41                let err = err.extend_with(|_, e| match async_graphql::Value::try_from(value) {
42                    Ok(value) => e.set(key, value),
43                    Err(err) => tracing::error!("Couldn't deserialize error value: {err}"),
44                });
45                Box::new(Self::Async(err, ctx))
46            }
47            Self::Custom(err) => {
48                let err = err.with_property(key, value);
49                Box::new(Self::Custom(err))
50            }
51        }
52    }
53
54    /// Checks wether this error is unexpected or not
55    fn is_unexpected(&self) -> bool {
56        match self {
57            // errors from the graphql lib are unexpected
58            GraphQLError::Async(_, _) => true,
59            GraphQLError::Custom(err) => err.unexpected,
60        }
61    }
62
63    /// Returns the string representation of the error
64    pub fn to_string(&self, include_context: bool) -> String {
65        match self {
66            GraphQLError::Async(err, context) => {
67                let code = GenericErrorCode::InternalServerError;
68                let status = code.status();
69                if include_context {
70                    format!(
71                        "[{} {}] {}: {}\n{}",
72                        status.as_str(),
73                        status.canonical_reason().unwrap_or("Unknown"),
74                        code.code(),
75                        &err.message,
76                        &context
77                    )
78                } else {
79                    format!(
80                        "[{} {}] {}: {}",
81                        status.as_str(),
82                        status.canonical_reason().unwrap_or("Unknown"),
83                        code.code(),
84                        &err.message
85                    )
86                }
87            }
88            GraphQLError::Custom(err) => {
89                if include_context {
90                    format!("{err:#}")
91                } else {
92                    format!("{err}")
93                }
94            }
95        }
96    }
97}
98
99impl From<async_graphql::Error> for Box<GraphQLError> {
100    fn from(err: async_graphql::Error) -> Self {
101        Box::new(GraphQLError::Async(err, SpanTrace::capture()))
102    }
103}
104impl From<Box<Error>> for Box<GraphQLError> {
105    fn from(err: Box<Error>) -> Self {
106        GraphQLError::from_err(err)
107    }
108}
109
110impl From<Box<GraphQLError>> for async_graphql::Error {
111    fn from(value: Box<GraphQLError>) -> Self {
112        let e = *value;
113
114        // Trace the error when converting to async_graphql error, which is done just before responding to requests
115        let new_error = match &e {
116            GraphQLError::Async(err, _) => err
117                .extensions
118                .as_ref()
119                .map(|e| e.get("statusCode").is_none())
120                .unwrap_or(true),
121            GraphQLError::Custom(_) => true,
122        };
123        if new_error {
124            if e.is_unexpected() {
125                tracing::error!("{}", e.to_string(true))
126            } else if tracing::event_enabled!(tracing::Level::DEBUG) {
127                tracing::warn!("{}", e.to_string(true))
128            } else {
129                tracing::warn!("{}", e.to_string(false))
130            }
131        }
132
133        // Convert type
134        let (gql_err, err_info): (async_graphql::Error, Option<Arc<dyn ErrorInfo + Send + Sync + 'static>>) = match e {
135            GraphQLError::Async(mut err, _) => {
136                if new_error {
137                    // Hide the message and provide generic internal error info
138                    err.source = Some(Arc::new(err.message));
139                    err.message = GenericErrorCode::InternalServerError.raw_message().into();
140                    (err, Some(Arc::new(GenericErrorCode::InternalServerError)))
141                } else {
142                    // Already converted
143                    (err, None)
144                }
145            }
146            GraphQLError::Custom(err) => {
147                let err = *err;
148                let source = err.source.map(|s| {
149                    let source: Arc<dyn Any + Send + Sync> = Arc::new(s);
150                    source
151                });
152                let async_err = async_graphql::Error {
153                    message: err.info.message(),
154                    source,
155                    extensions: None,
156                }
157                .extend_with(|_, e| {
158                    if let Some(prop) = err.properties {
159                        for (k, v) in prop.into_iter() {
160                            if k == "statusCode"
161                                || k == "statusKind"
162                                || k == "errorCode"
163                                || k == "rawMessage"
164                                || k == "messageFields"
165                            {
166                                tracing::error!("Error '{}' contains a reserved property: {}", err.info.code(), k);
167                                continue;
168                            }
169                            match async_graphql::Value::try_from(v) {
170                                Ok(v) => e.set(k, v),
171                                Err(err) => tracing::error!("Couldn't deserialize error value: {err}"),
172                            }
173                        }
174                    }
175                });
176                (async_err, Some(err.info))
177            }
178        };
179        if let Some(err_info) = err_info {
180            // Append error info properties
181            gql_err.extend_with(|_, e| {
182                let status = err_info.status();
183                e.set("statusCode", status.as_u16());
184                if let Some(reason) = status.canonical_reason() {
185                    e.set("statusKind", reason);
186                }
187                e.set("errorCode", err_info.code());
188                e.set("rawMessage", err_info.raw_message());
189                let fields = err_info.fields();
190                if !fields.is_empty() {
191                    let fields_map = IndexMap::from_iter(fields.into_iter().map(|(k, v)| (Name::new(k), v.into())));
192                    e.set("messageFields", fields_map);
193                }
194            })
195        } else {
196            gql_err
197        }
198    }
199}