Skip to main content

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 paramters 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 encoded URL query string parameters (also known as "search
165    /// params") into a GraphQL [`Request`].
166    ///
167    /// An error will be produced in the event that the query string parameters
168    /// cannot be turned into a valid GraphQL `Request`.
169    pub(crate) fn batch_from_urlencoded_query(
170        url_encoded_query: String,
171    ) -> Result<Vec<Request>, serde_json::Error> {
172        let value: Value = serde_urlencoded::from_bytes(url_encoded_query.as_bytes())
173            .map_err(serde_json::Error::custom)?;
174
175        Request::process_query_values(value)
176    }
177
178    /// Convert Bytes into a GraphQL [`Request`].
179    ///
180    /// An error will be produced in the event that the bytes array cannot be
181    /// turned into a valid GraphQL `Request`.
182    pub(crate) fn batch_from_bytes(bytes: &[u8]) -> Result<Vec<Request>, serde_json::Error> {
183        let value: Value = serde_json::from_slice(bytes).map_err(serde_json::Error::custom)?;
184
185        Request::process_batch_values(value)
186    }
187
188    fn allocate_result_array(value: &Value) -> Vec<Request> {
189        match value.as_array() {
190            Some(array) => Vec::with_capacity(array.len()),
191            None => Vec::with_capacity(1),
192        }
193    }
194
195    fn process_batch_values(value: Value) -> Result<Vec<Request>, serde_json::Error> {
196        let mut result = Request::allocate_result_array(&value);
197
198        if let Value::Array(entries) = value {
199            u64_histogram!(
200                "apollo.router.operations.batching.size",
201                "Number of queries contained within each query batch",
202                entries.len() as u64,
203                mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now
204            );
205
206            u64_counter!(
207                "apollo.router.operations.batching",
208                "Total requests with batched operations",
209                1,
210                mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now
211            );
212            for entry in entries {
213                let bytes = serde_json::to_vec(&entry)?;
214                result.push(Request::deserialize_from_bytes(&bytes.into())?);
215            }
216        } else {
217            let bytes = serde_json::to_vec(&value)?;
218            result.push(Request::deserialize_from_bytes(&bytes.into())?);
219        }
220        Ok(result)
221    }
222
223    fn process_query_values(value: Value) -> Result<Vec<Request>, serde_json::Error> {
224        let mut result = Request::allocate_result_array(&value);
225
226        if let Value::Array(entries) = value {
227            u64_histogram!(
228                "apollo.router.operations.batching.size",
229                "Number of queries contained within each query batch",
230                entries.len() as u64,
231                mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now
232            );
233
234            u64_counter!(
235                "apollo.router.operations.batching",
236                "Total requests with batched operations",
237                1,
238                mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now
239            );
240            for entry in entries {
241                result.push(Request::process_value(&entry)?);
242            }
243        } else {
244            result.push(Request::process_value(&value)?)
245        }
246        Ok(result)
247    }
248
249    fn process_value(value: &Value) -> Result<Request, serde_json::Error> {
250        let operation_name = value.get("operationName").and_then(Value::as_str);
251        let query = value.get("query").and_then(Value::as_str).map(String::from);
252        let variables: Object = value
253            .get("variables")
254            .and_then(Value::as_str)
255            .map(serde_json::from_str)
256            .transpose()?
257            .unwrap_or_default();
258        let extensions: Object = value
259            .get("extensions")
260            .and_then(Value::as_str)
261            .map(serde_json::from_str)
262            .transpose()?
263            .unwrap_or_default();
264
265        let request = Self::builder()
266            .and_query(query)
267            .variables(variables)
268            .and_operation_name(operation_name)
269            .extensions(extensions)
270            .build();
271        Ok(request)
272    }
273
274    /// Convert encoded URL query string parameters (also known as "search
275    /// params") into a GraphQL [`Request`].
276    ///
277    /// An error will be produced in the event that the query string parameters
278    /// cannot be turned into a valid GraphQL `Request`.
279    pub fn from_urlencoded_query(url_encoded_query: String) -> Result<Request, serde_json::Error> {
280        let urldecoded: Value = serde_urlencoded::from_bytes(url_encoded_query.as_bytes())
281            .map_err(serde_json::Error::custom)?;
282
283        Request::process_value(&urldecoded)
284    }
285}
286
287struct RequestFromBytesSeed<'data>(&'data Bytes);
288
289impl<'de> DeserializeSeed<'de> for RequestFromBytesSeed<'_> {
290    type Value = Request;
291
292    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
293    where
294        D: serde::Deserializer<'de>,
295    {
296        #[derive(serde::Deserialize)]
297        #[serde(field_identifier, rename_all = "camelCase")]
298        enum Field {
299            Query,
300            OperationName,
301            Variables,
302            Extensions,
303            #[serde(other)]
304            Other,
305        }
306
307        const FIELDS: &[&str] = &["query", "operationName", "variables", "extensions"];
308
309        struct RequestVisitor<'data>(&'data Bytes);
310
311        impl<'de> serde::de::Visitor<'de> for RequestVisitor<'_> {
312            type Value = Request;
313
314            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
315                formatter.write_str("a GraphQL request")
316            }
317
318            fn visit_map<V>(self, mut map: V) -> Result<Request, V::Error>
319            where
320                V: serde::de::MapAccess<'de>,
321            {
322                let mut query = None;
323                let mut operation_name = None;
324                let mut variables = None;
325                let mut extensions = None;
326                while let Some(key) = map.next_key()? {
327                    match key {
328                        Field::Query => {
329                            if query.is_some() {
330                                return Err(Error::duplicate_field("query"));
331                            }
332                            query = Some(map.next_value()?);
333                        }
334                        Field::OperationName => {
335                            if operation_name.is_some() {
336                                return Err(Error::duplicate_field("operationName"));
337                            }
338                            operation_name = Some(map.next_value()?);
339                        }
340                        Field::Variables => {
341                            if variables.is_some() {
342                                return Err(Error::duplicate_field("variables"));
343                            }
344                            let seed = serde_json_bytes::value::BytesSeed::new(self.0);
345                            let value = map.next_value_seed(seed)?;
346                            variables = Some(as_optional_object(value)?);
347                        }
348                        Field::Extensions => {
349                            if extensions.is_some() {
350                                return Err(Error::duplicate_field("extensions"));
351                            }
352                            let seed = serde_json_bytes::value::BytesSeed::new(self.0);
353                            let value = map.next_value_seed(seed)?;
354                            extensions = Some(as_optional_object(value)?);
355                        }
356                        Field::Other => {
357                            let _: serde::de::IgnoredAny = map.next_value()?;
358                        }
359                    }
360                }
361                Ok(Request {
362                    query: query.unwrap_or_default(),
363                    operation_name: operation_name.unwrap_or_default(),
364                    variables: variables.unwrap_or_default(),
365                    extensions: extensions.unwrap_or_default(),
366                })
367            }
368        }
369
370        deserializer.deserialize_struct("Request", FIELDS, RequestVisitor(self.0))
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use serde_json::json;
377    use serde_json_bytes::json as bjson;
378    use test_log::test;
379
380    use super::*;
381
382    #[test]
383    fn test_request() {
384        let data = json!(
385        {
386          "query": "query aTest($arg1: String!) { test(who: $arg1) }",
387          "operationName": "aTest",
388          "variables": { "arg1": "me" },
389          "extensions": {"extension": 1}
390        });
391        let result = check_deserialization(data);
392        assert_eq!(
393            result,
394            Request::builder()
395                .query("query aTest($arg1: String!) { test(who: $arg1) }".to_owned())
396                .operation_name("aTest")
397                .variables(bjson!({ "arg1": "me" }).as_object().unwrap().clone())
398                .extensions(bjson!({"extension": 1}).as_object().cloned().unwrap())
399                .build()
400        );
401    }
402
403    #[test]
404    fn test_no_variables() {
405        let result = check_deserialization(json!(
406        {
407          "query": "query aTest($arg1: String!) { test(who: $arg1) }",
408          "operationName": "aTest",
409          "extensions": {"extension": 1}
410        }));
411        assert_eq!(
412            result,
413            Request::builder()
414                .query("query aTest($arg1: String!) { test(who: $arg1) }".to_owned())
415                .operation_name("aTest")
416                .extensions(bjson!({"extension": 1}).as_object().cloned().unwrap())
417                .build()
418        );
419    }
420
421    #[test]
422    // rover sends { "variables": null } when running the introspection query,
423    // and possibly running other queries as well.
424    fn test_variables_is_null() {
425        let result = check_deserialization(json!(
426        {
427          "query": "query aTest($arg1: String!) { test(who: $arg1) }",
428          "operationName": "aTest",
429          "variables": null,
430          "extensions": {"extension": 1}
431        }));
432        assert_eq!(
433            result,
434            Request::builder()
435                .query("query aTest($arg1: String!) { test(who: $arg1) }")
436                .operation_name("aTest")
437                .extensions(bjson!({"extension": 1}).as_object().cloned().unwrap())
438                .build()
439        );
440    }
441
442    #[test]
443    fn from_urlencoded_query_works() {
444        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();
445
446        let expected_result = check_deserialization(json!(
447        {
448          "query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
449          "extensions": {
450              "persistedQuery": {
451                  "version": 1,
452                  "sha256Hash": "20a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f"
453              }
454            }
455        }));
456
457        let req = Request::from_urlencoded_query(query_string).unwrap();
458
459        assert_eq!(expected_result, req);
460    }
461
462    #[test]
463    fn from_urlencoded_query_with_variables_works() {
464        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();
465
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              "persistedQuery": {
472                  "version": 1,
473                  "sha256Hash": "20a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f"
474              }
475            }
476        }));
477
478        let req = Request::from_urlencoded_query(query_string).unwrap();
479
480        assert_eq!(expected_result, req);
481    }
482
483    #[test]
484    fn null_extensions() {
485        let expected_result = check_deserialization(json!(
486        {
487          "query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
488          "variables": {"date": "2022-01-01T00:00:00+00:00"},
489          "extensions": null
490        }));
491        insta::assert_yaml_snapshot!(expected_result);
492    }
493
494    #[test]
495    fn missing_extensions() {
496        let expected_result = check_deserialization(json!(
497        {
498          "query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
499          "variables": {"date": "2022-01-01T00:00:00+00:00"},
500        }));
501        insta::assert_yaml_snapshot!(expected_result);
502    }
503
504    #[test]
505    fn extensions() {
506        let expected_result = check_deserialization(json!(
507        {
508          "query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
509          "variables": {"date": "2022-01-01T00:00:00+00:00"},
510          "extensions": {
511            "something_simple": "else",
512            "something_complex": {
513                "nested": "value"
514            }
515          }
516        }));
517        insta::assert_yaml_snapshot!(expected_result);
518    }
519
520    fn check_deserialization(request: serde_json::Value) -> Request {
521        // check that deserialize_from_bytes agrees with Deserialize impl
522
523        let string = serde_json::to_string(&request).expect("could not serialize request");
524        let string_deserialized =
525            serde_json::from_str(&string).expect("could not deserialize string");
526        let bytes = Bytes::copy_from_slice(string.as_bytes());
527        let bytes_deserialized =
528            Request::deserialize_from_bytes(&bytes).expect("could not deserialize from bytes");
529        assert_eq!(
530            string_deserialized, bytes_deserialized,
531            "string and bytes deserialization did not match"
532        );
533        string_deserialized
534    }
535}