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;
9
10use apollo_compiler::response::GraphQLError as CompilerExecutionError;
11use apollo_compiler::response::ResponseDataPathSegment;
12use futures::Stream;
13use heck::ToShoutySnakeCase;
14pub use request::Request;
15pub use response::IncrementalResponse;
16use response::MalformedResponseError;
17pub use response::Response;
18use serde::Deserialize;
19use serde::Serialize;
20use serde_json_bytes::ByteString;
21use serde_json_bytes::Map as JsonMap;
22use serde_json_bytes::Value;
23pub(crate) use visitor::ResponseVisitor;
24
25use crate::json_ext::Object;
26use crate::json_ext::Path;
27pub use crate::json_ext::Path as JsonPath;
28pub use crate::json_ext::PathElement as JsonPathElement;
29
30/// An asynchronous [`Stream`] of GraphQL [`Response`]s.
31///
32/// In some cases such as with `@defer`, a single HTTP response from the Router
33/// may contain multiple GraphQL responses that will be sent at different times
34/// (as more data becomes available).
35///
36/// We represent this in Rust as a stream,
37/// even if that stream happens to only contain one item.
38pub type ResponseStream = Pin<Box<dyn Stream<Item = Response> + Send>>;
39
40#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
41#[serde(rename_all = "camelCase")]
42/// The error location
43pub struct Location {
44    /// The line number
45    pub line: u32,
46    /// The column number
47    pub column: u32,
48}
49
50/// A [GraphQL error](https://spec.graphql.org/October2021/#sec-Errors)
51/// as may be found in the `errors` field of a GraphQL [`Response`].
52///
53/// Converted to (or from) JSON with serde.
54#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
55#[serde(rename_all = "camelCase")]
56#[non_exhaustive]
57pub struct Error {
58    /// The error message.
59    pub message: String,
60
61    /// The locations of the error in the GraphQL document of the originating request.
62    #[serde(skip_serializing_if = "Vec::is_empty", default)]
63    pub locations: Vec<Location>,
64
65    /// If this is a field error, the JSON path to that field in [`Response::data`]
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub path: Option<Path>,
68
69    /// The optional GraphQL extensions for this error.
70    #[serde(default, skip_serializing_if = "Object::is_empty")]
71    pub extensions: Object,
72}
73// Implement getter and getter_mut to not use pub field directly
74
75#[buildstructor::buildstructor]
76impl Error {
77    /// Returns a builder that builds a GraphQL [`Error`] from its components.
78    ///
79    /// Builder methods:
80    ///
81    /// * `.message(impl Into<`[`String`]`>)`
82    ///   Required.
83    ///   Sets [`Error::message`].
84    ///
85    /// * `.locations(impl Into<`[`Vec`]`<`[`Location`]`>>)`
86    ///   Optional.
87    ///   Sets the entire `Vec` of [`Error::locations`], which defaults to the empty.
88    ///
89    /// * `.location(impl Into<`[`Location`]`>)`
90    ///   Optional, may be called multiple times.
91    ///   Adds one item at the end of [`Error::locations`].
92    ///
93    /// * `.path(impl Into<`[`Path`]`>)`
94    ///   Optional.
95    ///   Sets [`Error::path`].
96    ///
97    /// * `.extensions(impl Into<`[`serde_json_bytes::Map`]`<`[`ByteString`]`, `[`Value`]`>>)`
98    ///   Optional.
99    ///   Sets the entire [`Error::extensions`] map, which defaults to empty.
100    ///
101    /// * `.extension(impl Into<`[`ByteString`]`>, impl Into<`[`Value`]`>)`
102    ///   Optional, may be called multiple times.
103    ///   Adds one item to the [`Error::extensions`] map.
104    ///
105    /// * `.build()`
106    ///   Finishes the builder and returns a GraphQL [`Error`].
107    #[builder(visibility = "pub")]
108    fn new<T: Into<String>>(
109        message: String,
110        locations: Vec<Location>,
111        path: Option<Path>,
112        extension_code: T,
113        // Skip the `Object` type alias in order to use buildstructor’s map special-casing
114        mut extensions: JsonMap<ByteString, Value>,
115    ) -> Self {
116        extensions
117            .entry("code")
118            .or_insert_with(|| extension_code.into().into());
119        Self {
120            message,
121            locations,
122            path,
123            extensions,
124        }
125    }
126
127    pub(crate) fn from_value(value: Value) -> Result<Error, MalformedResponseError> {
128        let mut object = ensure_object!(value).map_err(|error| MalformedResponseError {
129            reason: format!("invalid error within `errors`: {}", error),
130        })?;
131
132        let extensions =
133            extract_key_value_from_object!(object, "extensions", Value::Object(o) => o)
134                .map_err(|err| MalformedResponseError {
135                    reason: format!("invalid `extensions` within error: {}", err),
136                })?
137                .unwrap_or_default();
138        let message = match extract_key_value_from_object!(object, "message", Value::String(s) => s)
139        {
140            Ok(Some(s)) => Ok(s.as_str().to_string()),
141            Ok(None) => Err(MalformedResponseError {
142                reason: "missing required `message` property within error".to_owned(),
143            }),
144            Err(err) => Err(MalformedResponseError {
145                reason: format!("invalid `message` within error: {}", err),
146            }),
147        }?;
148        let locations = extract_key_value_from_object!(object, "locations")
149            .map(skip_invalid_locations)
150            .map(serde_json_bytes::from_value)
151            .transpose()
152            .map_err(|err| MalformedResponseError {
153                reason: format!("invalid `locations` within error: {}", err),
154            })?
155            .unwrap_or_default();
156        let path = extract_key_value_from_object!(object, "path")
157            .map(serde_json_bytes::from_value)
158            .transpose()
159            .map_err(|err| MalformedResponseError {
160                reason: format!("invalid `path` within error: {}", err),
161            })?;
162
163        Ok(Error {
164            message,
165            locations,
166            path,
167            extensions,
168        })
169    }
170}
171
172/// GraphQL spec require that both "line" and "column" are positive numbers.
173/// However GraphQL Java and GraphQL Kotlin return `{ "line": -1, "column": -1 }`
174/// if they can't determine error location inside query.
175/// This function removes such locations from suplied value.
176fn skip_invalid_locations(mut value: Value) -> Value {
177    if let Some(array) = value.as_array_mut() {
178        array.retain(|location| {
179            location.get("line") != Some(&Value::from(-1))
180                || location.get("column") != Some(&Value::from(-1))
181        })
182    }
183    value
184}
185
186/// Displays (only) the error message.
187impl fmt::Display for Error {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        self.message.fmt(f)
190    }
191}
192
193/// Trait used to convert expected errors into a list of GraphQL errors
194pub(crate) trait IntoGraphQLErrors
195where
196    Self: Sized,
197{
198    fn into_graphql_errors(self) -> Result<Vec<Error>, Self>;
199}
200
201/// Trait used to get extension type from an error
202pub(crate) trait ErrorExtension
203where
204    Self: Sized,
205{
206    fn extension_code(&self) -> String {
207        std::any::type_name::<Self>().to_shouty_snake_case()
208    }
209
210    fn custom_extension_details(&self) -> Option<Object> {
211        None
212    }
213}
214
215impl From<CompilerExecutionError> for Error {
216    fn from(error: CompilerExecutionError) -> Self {
217        let CompilerExecutionError {
218            message,
219            locations,
220            path,
221            extensions,
222        } = error;
223        let locations = locations
224            .into_iter()
225            .map(|location| Location {
226                line: location.line as u32,
227                column: location.column as u32,
228            })
229            .collect::<Vec<_>>();
230        let path = if !path.is_empty() {
231            let elements = path
232                .into_iter()
233                .map(|element| match element {
234                    ResponseDataPathSegment::Field(name) => {
235                        JsonPathElement::Key(name.as_str().to_owned(), None)
236                    }
237                    ResponseDataPathSegment::ListIndex(i) => JsonPathElement::Index(i),
238                })
239                .collect();
240            Some(Path(elements))
241        } else {
242            None
243        };
244        Self {
245            message,
246            locations,
247            path,
248            extensions,
249        }
250    }
251}