Skip to main content

apollo_router/graphql/
mod.rs

1//! Types related to GraphQL requests, responses, etc.
2
3mod request;
4mod response;
5mod visitor;
6
7use std::fmt;
8use std::pin::Pin;
9use std::str::FromStr;
10
11use apollo_compiler::response::GraphQLError as CompilerExecutionError;
12use apollo_compiler::response::ResponseDataPathSegment;
13use futures::Stream;
14use heck::ToShoutySnakeCase;
15pub use request::Request;
16pub use response::IncrementalResponse;
17use response::MalformedResponseError;
18pub use response::Response;
19use serde::Deserialize;
20use serde::Serialize;
21use serde_json_bytes::ByteString;
22use serde_json_bytes::Map as JsonMap;
23use serde_json_bytes::Value;
24use uuid::Uuid;
25pub(crate) use visitor::ResponseVisitor;
26
27use crate::json_ext::Object;
28use crate::json_ext::Path;
29pub use crate::json_ext::Path as JsonPath;
30pub use crate::json_ext::PathElement as JsonPathElement;
31use crate::spec::query::ERROR_CODE_RESPONSE_VALIDATION;
32
33/// An asynchronous [`Stream`] of GraphQL [`Response`]s.
34///
35/// In some cases such as with `@defer`, a single HTTP response from the Router
36/// may contain multiple GraphQL responses that will be sent at different times
37/// (as more data becomes available).
38///
39/// We represent this in Rust as a stream,
40/// even if that stream happens to only contain one item.
41pub type ResponseStream = Pin<Box<dyn Stream<Item = Response> + Send>>;
42
43#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
44#[serde(rename_all = "camelCase")]
45/// The error location
46pub struct Location {
47    /// The line number
48    pub line: u32,
49    /// The column number
50    pub column: u32,
51}
52
53/// A [GraphQL error](https://spec.graphql.org/October2021/#sec-Errors)
54/// as may be found in the `errors` field of a GraphQL [`Response`].
55///
56/// Converted to (or from) JSON with serde.
57#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase", default)]
59#[non_exhaustive]
60pub struct Error {
61    /// The error message.
62    pub message: String,
63
64    /// The locations of the error in the GraphQL document of the originating request.
65    #[serde(skip_serializing_if = "Vec::is_empty")]
66    pub locations: Vec<Location>,
67
68    /// If this is a field error, the JSON path to that field in [`Response::data`]
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub path: Option<Path>,
71
72    /// The optional GraphQL extensions for this error.
73    #[serde(skip_serializing_if = "Object::is_empty")]
74    pub extensions: Object,
75
76    /// A unique identifier for this error
77    #[serde(skip_serializing)]
78    apollo_id: Uuid,
79
80    /// Set when an upstream site (connectors, demand_control) has already emitted a span
81    /// event for this error. Read by the centralized emit so traces carry exactly one event
82    /// per error. Never serialized — must not leak into the user-facing response.
83    #[serde(skip)]
84    span_event_emitted: bool,
85}
86
87impl Default for Error {
88    fn default() -> Self {
89        Self {
90            message: String::new(),
91            locations: Vec::new(),
92            path: None,
93            extensions: Object::new(),
94            apollo_id: generate_uuid(),
95            span_event_emitted: false,
96        }
97    }
98}
99
100// Implement getter and getter_mut to not use pub field directly
101
102#[buildstructor::buildstructor]
103impl Error {
104    /// Returns a builder that builds a GraphQL [`Error`] from its components.
105    ///
106    /// Builder methods:
107    ///
108    /// * `.message(impl Into<`[`String`]`>)`
109    ///   Required.
110    ///   Sets [`Error::message`].
111    ///
112    /// * `.locations(impl Into<`[`Vec`]`<`[`Location`]`>>)`
113    ///   Optional.
114    ///   Sets the entire `Vec` of [`Error::locations`], which defaults to the empty.
115    ///
116    /// * `.location(impl Into<`[`Location`]`>)`
117    ///   Optional, may be called multiple times.
118    ///   Adds one item at the end of [`Error::locations`].
119    ///
120    /// * `.path(impl Into<`[`Path`]`>)`
121    ///   Optional.
122    ///   Sets [`Error::path`].
123    ///
124    /// * `.extensions(impl Into<`[`serde_json_bytes::Map`]`<`[`ByteString`]`, `[`Value`]`>>)`
125    ///   Optional.
126    ///   Sets the entire [`Error::extensions`] map, which defaults to empty.
127    ///
128    /// * `.extension(impl Into<`[`ByteString`]`>, impl Into<`[`Value`]`>)`
129    ///   Optional, may be called multiple times.
130    ///   Adds one item to the [`Error::extensions`] map.
131    ///
132    /// * `.extension_code(impl Into<`[`String`]`>)`
133    ///   Optional.
134    ///   Sets the "code" in the extension map. Will be ignored if extension already has this key
135    ///   set.
136    ///
137    /// * `.apollo_id(impl Into<`[`Uuid`]`>)`
138    ///   Optional.
139    ///   Sets the unique identifier for this Error. This should only be used in cases of
140    ///   deserialization or testing. If not given, the ID will be auto-generated.
141    ///
142    /// * `.build()`
143    ///   Finishes the builder and returns a GraphQL [`Error`].
144    #[builder(visibility = "pub")]
145    fn new(
146        message: String,
147        locations: Vec<Location>,
148        path: Option<Path>,
149        extension_code: Option<String>,
150        // Skip the `Object` type alias in order to use buildstructor's map special-casing
151        mut extensions: JsonMap<ByteString, Value>,
152        apollo_id: Option<Uuid>,
153    ) -> Self {
154        if let Some(code) = extension_code {
155            extensions
156                .entry("code")
157                .or_insert(Value::String(ByteString::from(code)));
158        }
159        Self {
160            message,
161            locations,
162            path,
163            extensions,
164            apollo_id: apollo_id.unwrap_or_else(Uuid::new_v4),
165            span_event_emitted: false,
166        }
167    }
168
169    pub(crate) fn from_value(value: Value) -> Result<Error, MalformedResponseError> {
170        let mut object = ensure_object!(value).map_err(|error| MalformedResponseError {
171            reason: format!("invalid error within `errors`: {error}"),
172        })?;
173
174        let extensions =
175            extract_key_value_from_object!(object, "extensions", Value::Object(o) => o)
176                .map_err(|err| MalformedResponseError {
177                    reason: format!("invalid `extensions` within error: {err}"),
178                })?
179                .unwrap_or_default();
180        let message = match extract_key_value_from_object!(object, "message", Value::String(s) => s)
181        {
182            Ok(Some(s)) => Ok(s.as_str().to_string()),
183            Ok(None) => Err(MalformedResponseError {
184                reason: "missing required `message` property within error".to_owned(),
185            }),
186            Err(err) => Err(MalformedResponseError {
187                reason: format!("invalid `message` within error: {err}"),
188            }),
189        }?;
190        let locations = extract_key_value_from_object!(object, "locations")
191            .map(skip_invalid_locations)
192            .map(serde_json_bytes::from_value)
193            .transpose()
194            .map_err(|err| MalformedResponseError {
195                reason: format!("invalid `locations` within error: {err}"),
196            })?
197            .unwrap_or_default();
198        let path = extract_key_value_from_object!(object, "path")
199            .map(serde_json_bytes::from_value)
200            .transpose()
201            .map_err(|err| MalformedResponseError {
202                reason: format!("invalid `path` within error: {err}"),
203            })?;
204        let apollo_id: Option<Uuid> = extract_key_value_from_object!(
205            object,
206            "apolloId",
207            Value::String(s) => s
208        )
209        .map_err(|err| MalformedResponseError {
210            reason: format!("invalid `apolloId` within error: {err}"),
211        })?
212        .map(|s| {
213            Uuid::from_str(s.as_str()).map_err(|err| MalformedResponseError {
214                reason: format!("invalid `apolloId` within error: {err}"),
215            })
216        })
217        .transpose()?;
218
219        Ok(Self::new(
220            message, locations, path, None, extensions, apollo_id,
221        ))
222    }
223
224    pub(crate) fn from_value_completion_value(value: &Value) -> Option<Error> {
225        let value_completion = ensure_object!(value).ok()?;
226        let mut extensions = value_completion
227            .get("extensions")
228            .and_then(|e: &Value| -> Option<Object> {
229                serde_json_bytes::from_value(e.clone()).ok()
230            })
231            .unwrap_or_default();
232        extensions.insert("code", ERROR_CODE_RESPONSE_VALIDATION.into());
233        extensions.insert("severity", tracing::Level::WARN.as_str().into());
234
235        let message = value_completion
236            .get("message")
237            .and_then(|m| m.as_str())
238            .map(|m| m.to_string())
239            .unwrap_or_default();
240        let locations = value_completion
241            .get("locations")
242            .map(|l: &Value| skip_invalid_locations(l.clone()))
243            .map(|l: Value| serde_json_bytes::from_value(l).unwrap_or_default())
244            .unwrap_or_default();
245        let path =
246            value_completion
247                .get("path")
248                .and_then(|p: &serde_json_bytes::Value| -> Option<Path> {
249                    serde_json_bytes::from_value(p.clone()).ok()
250                });
251
252        Some(Self::new(
253            message, locations, path, None, extensions,
254            None, // apollo_id is not serialized, so it will never exist in a serialized vc error
255        ))
256    }
257
258    /// Extract the error code from [`Error::extensions`] as a String if it is set.
259    pub fn extension_code(&self) -> Option<String> {
260        self.extensions.get("code").and_then(|c| match c {
261            Value::String(s) => Some(s.as_str().to_owned()),
262            Value::Number(n) => Some(n.to_string()),
263            Value::Null | Value::Array(_) | Value::Object(_) | Value::Bool(_) => None,
264        })
265    }
266
267    /// Retrieve the internal Apollo unique ID for this error
268    pub fn apollo_id(&self) -> Uuid {
269        self.apollo_id
270    }
271
272    /// Returns a duplicate of the error where [`self.apollo_id`][Self::apollo_id] is now the given ID
273    pub fn with_apollo_id(&self, id: Uuid) -> Self {
274        let mut new_err = self.clone();
275        new_err.apollo_id = id;
276        new_err
277    }
278
279    pub(crate) fn span_event_emitted(&self) -> bool {
280        self.span_event_emitted
281    }
282
283    pub(crate) fn set_span_event_emitted(&mut self, value: bool) {
284        self.span_event_emitted = value;
285    }
286
287    #[cfg(test)]
288    /// Returns a duplicate of the error where [`self.apollo_id`] is `Uuid::nil()`. Used for
289    /// comparing errors in tests where you cannot control the randomly generated Uuid
290    pub fn with_null_id(&self) -> Self {
291        self.with_apollo_id(Uuid::nil())
292    }
293}
294
295/// Generate a random Uuid. For use in generating a default [`Error::apollo_id`] when not supplied
296/// during deserialization.
297fn generate_uuid() -> Uuid {
298    Uuid::new_v4()
299}
300
301/// GraphQL spec require that both "line" and "column" are positive numbers.
302/// However GraphQL Java and GraphQL Kotlin return `{ "line": -1, "column": -1 }`
303/// if they can't determine error location inside query.
304/// This function removes such locations from supplied value.
305fn skip_invalid_locations(mut value: Value) -> Value {
306    if let Some(array) = value.as_array_mut() {
307        array.retain(|location| {
308            location.get("line") != Some(&Value::from(-1))
309                || location.get("column") != Some(&Value::from(-1))
310        })
311    }
312    value
313}
314
315/// Displays (only) the error message.
316impl fmt::Display for Error {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        self.message.fmt(f)
319    }
320}
321
322/// Trait used to convert expected errors into a list of GraphQL errors
323pub(crate) trait IntoGraphQLErrors
324where
325    Self: Sized,
326{
327    fn into_graphql_errors(self) -> Result<Vec<Error>, Self>;
328}
329
330/// Trait used to get extension type from an error
331pub(crate) trait ErrorExtension
332where
333    Self: Sized,
334{
335    fn extension_code(&self) -> String {
336        std::any::type_name::<Self>().to_shouty_snake_case()
337    }
338
339    fn custom_extension_details(&self) -> Option<Object> {
340        None
341    }
342}
343
344impl From<CompilerExecutionError> for Error {
345    fn from(error: CompilerExecutionError) -> Self {
346        let CompilerExecutionError {
347            message,
348            locations,
349            path,
350            extensions,
351        } = error;
352        let locations = locations
353            .into_iter()
354            .map(|location| Location {
355                line: location.line as u32,
356                column: location.column as u32,
357            })
358            .collect::<Vec<_>>();
359        let path = if !path.is_empty() {
360            let elements = path
361                .into_iter()
362                .map(|element| match element {
363                    ResponseDataPathSegment::Field(name) => {
364                        JsonPathElement::Key(name.as_str().to_owned(), None)
365                    }
366                    ResponseDataPathSegment::ListIndex(i) => JsonPathElement::Index(i),
367                })
368                .collect();
369            Some(Path(elements))
370        } else {
371            None
372        };
373        Self {
374            message,
375            locations,
376            path,
377            extensions,
378            apollo_id: Uuid::new_v4(),
379            span_event_emitted: false,
380        }
381    }
382}
383
384/// Assert that the expected and actual [`Error`] are equal when ignoring their
385/// [`Error::apollo_id`].
386#[macro_export]
387macro_rules! assert_error_eq_ignoring_id {
388    ($expected:expr, $actual:expr) => {
389        assert_eq!($expected.with_null_id(), $actual.with_null_id());
390    };
391}
392
393/// Assert that the expected and actual lists of [`Error`] are equal when ignoring their
394/// [`Error::apollo_id`].
395#[macro_export]
396macro_rules! assert_errors_eq_ignoring_id {
397    ($expected:expr, $actual:expr) => {{
398        let normalize =
399            |v: &[graphql::Error]| v.iter().map(|e| e.with_null_id()).collect::<Vec<_>>();
400
401        assert_eq!(normalize(&$expected), normalize(&$actual));
402    }};
403}
404
405/// Assert that the expected and actual [`Response`] are equal when ignoring the
406/// [`Error::apollo_id`] on any [`Error`] in their [`Response::errors`].
407#[macro_export]
408macro_rules! assert_response_eq_ignoring_error_id {
409    ($expected:expr, $actual:expr) => {{
410        let normalize =
411            |v: &[graphql::Error]| v.iter().map(|e| e.with_null_id()).collect::<Vec<_>>();
412        let mut expected_response: graphql::Response = $expected.clone();
413        let mut actual_response: graphql::Response = $actual.clone();
414        expected_response.errors = normalize(&expected_response.errors);
415        actual_response.errors = normalize(&actual_response.errors);
416
417        assert_eq!(expected_response, actual_response);
418    }};
419}