apollo_router/graphql/
request.rs

1use bytes::Bytes;
2use derivative::Derivative;
3use serde::Deserialize;
4use serde::Serialize;
5use serde::de::DeserializeSeed;
6use serde::de::Error;
7use serde_json_bytes::ByteString;
8use serde_json_bytes::Map as JsonMap;
9
10use crate::configuration::BatchingMode;
11use crate::json_ext::Object;
12use crate::json_ext::Value;
13
14/// A GraphQL `Request` used to represent both supergraph and subgraph requests.
15#[derive(Clone, Derivative, Serialize, Deserialize, Default)]
16// Note: if adding #[serde(deny_unknown_fields)],
17// also remove `Fields::Other` in `DeserializeSeed` impl.
18#[serde(rename_all = "camelCase")]
19#[derivative(Debug, PartialEq, Eq, Hash)]
20#[non_exhaustive]
21pub struct Request {
22    /// The GraphQL operation (e.g., query, mutation) string.
23    ///
24    /// For historical purposes, the term "query" is commonly used to refer to
25    /// *any* GraphQL operation which might be, e.g., a `mutation`.
26    #[serde(skip_serializing_if = "Option::is_none", default)]
27    pub query: Option<String>,
28
29    /// The (optional) GraphQL operation name.
30    ///
31    /// When specified, this name must match the name of an operation in the
32    /// GraphQL document.  When excluded, there must exist only a single
33    /// operation in the GraphQL document.  Typically, this value is provided as
34    /// the `operationName` on an HTTP-sourced GraphQL request.
35    #[serde(skip_serializing_if = "Option::is_none", default)]
36    pub operation_name: Option<String>,
37
38    /// The (optional) GraphQL variables in the form of a JSON object.
39    ///
40    /// When specified, these variables can be referred to in the `query` by
41    /// using `$variableName` syntax, where `{"variableName": "value"}` has been
42    /// specified as this `variables` value.
43    #[serde(
44        skip_serializing_if = "Object::is_empty",
45        default,
46        deserialize_with = "deserialize_null_default"
47    )]
48    pub variables: Object,
49
50    /// The (optional) GraphQL `extensions` of a GraphQL request.
51    ///
52    /// The implementations of extensions are server specific and not specified by
53    /// the GraphQL specification.
54    /// For example, Apollo projects support [Automated Persisted Queries][APQ]
55    /// which are specified in the `extensions` of a request by populating the
56    /// `persistedQuery` key within the `extensions` object:
57    ///
58    /// ```json
59    /// {
60    ///   "query": "...",
61    ///   "variables": { /* ... */ },
62    ///   "extensions": {
63    ///     "persistedQuery": {
64    ///       "version": 1,
65    ///       "sha256Hash": "sha256HashOfQuery"
66    /// .   }
67    ///   }
68    /// }
69    /// ```
70    ///
71    /// [APQ]: https://www.apollographql.com/docs/apollo-server/performance/apq/
72    /// Note we allow null when deserializing as per [graphql-over-http spec](https://graphql.github.io/graphql-over-http/draft/#sel-EALFPCCBCEtC37P)
73    #[serde(
74        skip_serializing_if = "Object::is_empty",
75        default,
76        deserialize_with = "deserialize_null_default"
77    )]
78    pub extensions: Object,
79}
80
81// NOTE: this deserialize helper is used to transform `null` to Default::default()
82fn deserialize_null_default<'de, D, T: Default + Deserialize<'de>>(
83    deserializer: D,
84) -> Result<T, D::Error>
85where
86    D: serde::Deserializer<'de>,
87{
88    <Option<T>>::deserialize(deserializer).map(|x| x.unwrap_or_default())
89}
90
91fn as_optional_object<E: Error>(value: Value) -> Result<Object, E> {
92    use serde::de::Unexpected;
93
94    let exp = "a map or null";
95    match value {
96        Value::Object(object) => Ok(object),
97        // Similar to `deserialize_null_default`:
98        Value::Null => Ok(Object::default()),
99        Value::Bool(value) => Err(E::invalid_type(Unexpected::Bool(value), &exp)),
100        Value::Number(_) => Err(E::invalid_type(Unexpected::Other("a number"), &exp)),
101        Value::String(value) => Err(E::invalid_type(Unexpected::Str(value.as_str()), &exp)),
102        Value::Array(_) => Err(E::invalid_type(Unexpected::Seq, &exp)),
103    }
104}
105
106#[buildstructor::buildstructor]
107impl Request {
108    #[builder(visibility = "pub")]
109    /// This is the constructor (or builder) to use when constructing a GraphQL
110    /// `Request`.
111    ///
112    /// The optionality of parameters on this constructor match the runtime
113    /// requirements which are necessary to create a valid GraphQL `Request`.
114    /// (This contrasts against the `fake_new` constructor which may be more
115    /// tolerant to missing properties which are only necessary for testing
116    /// purposes.  If you are writing tests, you may want to use `Self::fake_new()`.)
117    fn new(
118        query: Option<String>,
119        operation_name: Option<String>,
120        // Skip the `Object` type alias in order to use buildstructor’s map special-casing
121        variables: JsonMap<ByteString, Value>,
122        extensions: JsonMap<ByteString, Value>,
123    ) -> Self {
124        Self {
125            query,
126            operation_name,
127            variables,
128            extensions,
129        }
130    }
131
132    #[builder(visibility = "pub")]
133    /// This is the constructor (or builder) to use when constructing a **fake**
134    /// GraphQL `Request`.  Use `Self::new()` to construct a _real_ request.
135    ///
136    /// This is offered for testing purposes and it relaxes the requirements
137    /// of some parameters to be specified that would be otherwise required
138    /// for a real request.  It's usually enough for most testing purposes,
139    /// especially when a fully constructed `Request` is difficult to construct.
140    /// While today, its parameters have the same optionality as its `new`
141    /// counterpart, that may change in future versions.
142    fn fake_new(
143        query: Option<String>,
144        operation_name: Option<String>,
145        // Skip the `Object` type alias in order to use buildstructor’s map special-casing
146        variables: JsonMap<ByteString, Value>,
147        extensions: JsonMap<ByteString, Value>,
148    ) -> Self {
149        Self {
150            query,
151            operation_name,
152            variables,
153            extensions,
154        }
155    }
156
157    /// Deserialize as JSON from `&Bytes`, avoiding string copies where possible
158    pub fn deserialize_from_bytes(data: &Bytes) -> Result<Self, serde_json::Error> {
159        let seed = RequestFromBytesSeed(data);
160        let mut de = serde_json::Deserializer::from_slice(data);
161        seed.deserialize(&mut de)
162    }
163
164    /// Convert Bytes into a GraphQL [`Request`].
165    ///
166    /// An error will be produced in the event that the bytes array cannot be
167    /// turned into a valid GraphQL `Request`.
168    pub(crate) fn batch_from_bytes(bytes: &[u8]) -> Result<Vec<Request>, serde_json::Error> {
169        let value: Value = serde_json::from_slice(bytes).map_err(serde_json::Error::custom)?;
170
171        Request::process_batch_values(value)
172    }
173
174    fn allocate_result_array(value: &Value) -> Vec<Request> {
175        match value.as_array() {
176            Some(array) => Vec::with_capacity(array.len()),
177            None => Vec::with_capacity(1),
178        }
179    }
180
181    fn process_batch_values(value: Value) -> Result<Vec<Request>, serde_json::Error> {
182        let mut result = Request::allocate_result_array(&value);
183
184        if let Value::Array(entries) = value {
185            u64_histogram!(
186                "apollo.router.operations.batching.size",
187                "Number of queries contained within each query batch",
188                entries.len() as u64,
189                mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now
190            );
191
192            u64_counter!(
193                "apollo.router.operations.batching",
194                "Total requests with batched operations",
195                1,
196                mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now
197            );
198            for entry in entries {
199                let bytes = serde_json::to_vec(&entry)?;
200                result.push(Request::deserialize_from_bytes(&bytes.into())?);
201            }
202        } else {
203            let bytes = serde_json::to_vec(&value)?;
204            result.push(Request::deserialize_from_bytes(&bytes.into())?);
205        }
206        Ok(result)
207    }
208
209    fn process_value(value: &Value) -> Result<Request, serde_json::Error> {
210        let operation_name = value.get("operationName").and_then(Value::as_str);
211        let query = value.get("query").and_then(Value::as_str).map(String::from);
212        let variables: Object = value
213            .get("variables")
214            .and_then(Value::as_str)
215            .map(serde_json::from_str)
216            .transpose()?
217            .unwrap_or_default();
218        let extensions: Object = value
219            .get("extensions")
220            .and_then(Value::as_str)
221            .map(serde_json::from_str)
222            .transpose()?
223            .unwrap_or_default();
224
225        let request = Self::builder()
226            .and_query(query)
227            .variables(variables)
228            .and_operation_name(operation_name)
229            .extensions(extensions)
230            .build();
231        Ok(request)
232    }
233
234    /// Convert encoded URL query string parameters (also known as "search
235    /// params") into a GraphQL [`Request`].
236    ///
237    /// An error will be produced in the event that the query string parameters
238    /// cannot be turned into a valid GraphQL `Request`.
239    pub fn from_urlencoded_query(url_encoded_query: String) -> Result<Request, serde_json::Error> {
240        let urldecoded: Value = serde_urlencoded::from_bytes(url_encoded_query.as_bytes())
241            .map_err(serde_json::Error::custom)?;
242
243        Request::process_value(&urldecoded)
244    }
245}
246
247struct RequestFromBytesSeed<'data>(&'data Bytes);
248
249impl<'de> DeserializeSeed<'de> for RequestFromBytesSeed<'_> {
250    type Value = Request;
251
252    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
253    where
254        D: serde::Deserializer<'de>,
255    {
256        #[derive(serde::Deserialize)]
257        #[serde(field_identifier, rename_all = "camelCase")]
258        enum Field {
259            Query,
260            OperationName,
261            Variables,
262            Extensions,
263            #[serde(other)]
264            Other,
265        }
266
267        const FIELDS: &[&str] = &["query", "operationName", "variables", "extensions"];
268
269        struct RequestVisitor<'data>(&'data Bytes);
270
271        impl<'de> serde::de::Visitor<'de> for RequestVisitor<'_> {
272            type Value = Request;
273
274            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
275                formatter.write_str("a GraphQL request")
276            }
277
278            fn visit_map<V>(self, mut map: V) -> Result<Request, V::Error>
279            where
280                V: serde::de::MapAccess<'de>,
281            {
282                let mut query = None;
283                let mut operation_name = None;
284                let mut variables = None;
285                let mut extensions = None;
286                while let Some(key) = map.next_key()? {
287                    match key {
288                        Field::Query => {
289                            if query.is_some() {
290                                return Err(Error::duplicate_field("query"));
291                            }
292                            query = Some(map.next_value()?);
293                        }
294                        Field::OperationName => {
295                            if operation_name.is_some() {
296                                return Err(Error::duplicate_field("operationName"));
297                            }
298                            operation_name = Some(map.next_value()?);
299                        }
300                        Field::Variables => {
301                            if variables.is_some() {
302                                return Err(Error::duplicate_field("variables"));
303                            }
304                            let seed = serde_json_bytes::value::BytesSeed::new(self.0);
305                            let value = map.next_value_seed(seed)?;
306                            variables = Some(as_optional_object(value)?);
307                        }
308                        Field::Extensions => {
309                            if extensions.is_some() {
310                                return Err(Error::duplicate_field("extensions"));
311                            }
312                            let seed = serde_json_bytes::value::BytesSeed::new(self.0);
313                            let value = map.next_value_seed(seed)?;
314                            extensions = Some(as_optional_object(value)?);
315                        }
316                        Field::Other => {
317                            let _: serde::de::IgnoredAny = map.next_value()?;
318                        }
319                    }
320                }
321                Ok(Request {
322                    query: query.unwrap_or_default(),
323                    operation_name: operation_name.unwrap_or_default(),
324                    variables: variables.unwrap_or_default(),
325                    extensions: extensions.unwrap_or_default(),
326                })
327            }
328        }
329
330        deserializer.deserialize_struct("Request", FIELDS, RequestVisitor(self.0))
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use serde_json::json;
337    use serde_json_bytes::json as bjson;
338    use test_log::test;
339
340    use super::*;
341
342    #[test]
343    fn test_request() {
344        let data = json!(
345        {
346          "query": "query aTest($arg1: String!) { test(who: $arg1) }",
347          "operationName": "aTest",
348          "variables": { "arg1": "me" },
349          "extensions": {"extension": 1}
350        });
351        let result = check_deserialization(data);
352        assert_eq!(
353            result,
354            Request::builder()
355                .query("query aTest($arg1: String!) { test(who: $arg1) }".to_owned())
356                .operation_name("aTest")
357                .variables(bjson!({ "arg1": "me" }).as_object().unwrap().clone())
358                .extensions(bjson!({"extension": 1}).as_object().cloned().unwrap())
359                .build()
360        );
361    }
362
363    #[test]
364    fn test_no_variables() {
365        let result = check_deserialization(json!(
366        {
367          "query": "query aTest($arg1: String!) { test(who: $arg1) }",
368          "operationName": "aTest",
369          "extensions": {"extension": 1}
370        }));
371        assert_eq!(
372            result,
373            Request::builder()
374                .query("query aTest($arg1: String!) { test(who: $arg1) }".to_owned())
375                .operation_name("aTest")
376                .extensions(bjson!({"extension": 1}).as_object().cloned().unwrap())
377                .build()
378        );
379    }
380
381    #[test]
382    // rover sends { "variables": null } when running the introspection query,
383    // and possibly running other queries as well.
384    fn test_variables_is_null() {
385        let result = check_deserialization(json!(
386        {
387          "query": "query aTest($arg1: String!) { test(who: $arg1) }",
388          "operationName": "aTest",
389          "variables": null,
390          "extensions": {"extension": 1}
391        }));
392        assert_eq!(
393            result,
394            Request::builder()
395                .query("query aTest($arg1: String!) { test(who: $arg1) }")
396                .operation_name("aTest")
397                .extensions(bjson!({"extension": 1}).as_object().cloned().unwrap())
398                .build()
399        );
400    }
401
402    #[test]
403    fn from_urlencoded_query_works() {
404        let query_string = "query=%7B+topProducts+%7B+upc+name+reviews+%7B+id+product+%7B+name+%7D+author+%7B+id+name+%7D+%7D+%7D+%7D&extensions=%7B+%22persistedQuery%22+%3A+%7B+%22version%22+%3A+1%2C+%22sha256Hash%22+%3A+%2220a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f%22+%7D+%7D".to_string();
405
406        let expected_result = check_deserialization(json!(
407        {
408          "query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
409          "extensions": {
410              "persistedQuery": {
411                  "version": 1,
412                  "sha256Hash": "20a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f"
413              }
414            }
415        }));
416
417        let req = Request::from_urlencoded_query(query_string).unwrap();
418
419        assert_eq!(expected_result, req);
420    }
421
422    #[test]
423    fn from_urlencoded_query_with_variables_works() {
424        let query_string = "query=%7B+topProducts+%7B+upc+name+reviews+%7B+id+product+%7B+name+%7D+author+%7B+id+name+%7D+%7D+%7D+%7D&variables=%7B%22date%22%3A%222022-01-01T00%3A00%3A00%2B00%3A00%22%7D&extensions=%7B+%22persistedQuery%22+%3A+%7B+%22version%22+%3A+1%2C+%22sha256Hash%22+%3A+%2220a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f%22+%7D+%7D".to_string();
425
426        let expected_result = check_deserialization(json!(
427        {
428          "query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
429          "variables": {"date": "2022-01-01T00:00:00+00:00"},
430          "extensions": {
431              "persistedQuery": {
432                  "version": 1,
433                  "sha256Hash": "20a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f"
434              }
435            }
436        }));
437
438        let req = Request::from_urlencoded_query(query_string).unwrap();
439
440        assert_eq!(expected_result, req);
441    }
442
443    #[test]
444    fn null_extensions() {
445        let expected_result = check_deserialization(json!(
446        {
447          "query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
448          "variables": {"date": "2022-01-01T00:00:00+00:00"},
449          "extensions": null
450        }));
451        insta::assert_yaml_snapshot!(expected_result);
452    }
453
454    #[test]
455    fn missing_extensions() {
456        let expected_result = check_deserialization(json!(
457        {
458          "query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
459          "variables": {"date": "2022-01-01T00:00:00+00:00"},
460        }));
461        insta::assert_yaml_snapshot!(expected_result);
462    }
463
464    #[test]
465    fn extensions() {
466        let expected_result = check_deserialization(json!(
467        {
468          "query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
469          "variables": {"date": "2022-01-01T00:00:00+00:00"},
470          "extensions": {
471            "something_simple": "else",
472            "something_complex": {
473                "nested": "value"
474            }
475          }
476        }));
477        insta::assert_yaml_snapshot!(expected_result);
478    }
479
480    fn check_deserialization(request: serde_json::Value) -> Request {
481        // check that deserialize_from_bytes agrees with Deserialize impl
482
483        let string = serde_json::to_string(&request).expect("could not serialize request");
484        let string_deserialized =
485            serde_json::from_str(&string).expect("could not deserialize string");
486        let bytes = Bytes::copy_from_slice(string.as_bytes());
487        let bytes_deserialized =
488            Request::deserialize_from_bytes(&bytes).expect("could not deserialize from bytes");
489        assert_eq!(
490            string_deserialized, bytes_deserialized,
491            "string and bytes deserialization did not match"
492        );
493        string_deserialized
494    }
495}