Skip to main content

apollo_router/graphql/
response.rs

1#![allow(missing_docs)] // FIXME
2use std::time::Instant;
3
4use apollo_compiler::response::ExecutionResponse;
5use bytes::Bytes;
6use displaydoc::Display;
7use serde::Deserialize;
8use serde::Serialize;
9use serde_json_bytes::ByteString;
10use serde_json_bytes::Map;
11
12use crate::error::Error;
13use crate::graphql::IntoGraphQLErrors;
14use crate::json_ext::Object;
15use crate::json_ext::Path;
16use crate::json_ext::Value;
17
18#[derive(thiserror::Error, Display, Debug, Eq, PartialEq)]
19#[error("GraphQL response was malformed: {reason}")]
20pub(crate) struct MalformedResponseError {
21    /// The reason the deserialization failed.
22    pub(crate) reason: String,
23}
24
25/// A graphql primary response.
26/// Used for federated and subgraph queries.
27#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
28#[serde(rename_all = "camelCase")]
29#[non_exhaustive]
30pub struct Response {
31    /// The label that was passed to the defer or stream directive for this patch.
32    #[serde(skip_serializing_if = "Option::is_none", default)]
33    pub label: Option<String>,
34
35    /// The response data.
36    #[serde(skip_serializing_if = "Option::is_none", default)]
37    pub data: Option<Value>,
38
39    /// The path that the data should be merged at.
40    #[serde(skip_serializing_if = "Option::is_none", default)]
41    pub path: Option<Path>,
42
43    /// The optional graphql errors encountered.
44    #[serde(skip_serializing_if = "Vec::is_empty", default)]
45    pub errors: Vec<Error>,
46
47    /// The optional graphql extensions.
48    #[serde(skip_serializing_if = "Object::is_empty", default)]
49    pub extensions: Object,
50
51    #[serde(skip_serializing_if = "Option::is_none", default)]
52    pub has_next: Option<bool>,
53
54    #[serde(skip, default)]
55    pub subscribed: Option<bool>,
56
57    /// Used for subscription event to compute the duration of a subscription event
58    #[serde(skip, default)]
59    pub created_at: Option<Instant>,
60
61    #[serde(skip_serializing_if = "Vec::is_empty", default)]
62    pub incremental: Vec<IncrementalResponse>,
63}
64
65#[buildstructor::buildstructor]
66impl Response {
67    /// Constructor
68    #[builder(visibility = "pub")]
69    fn new(
70        label: Option<String>,
71        data: Option<Value>,
72        path: Option<Path>,
73        errors: Vec<Error>,
74        extensions: Map<ByteString, Value>,
75        _subselection: Option<String>,
76        has_next: Option<bool>,
77        subscribed: Option<bool>,
78        incremental: Vec<IncrementalResponse>,
79        created_at: Option<Instant>,
80    ) -> Self {
81        Self {
82            label,
83            data,
84            path,
85            errors,
86            extensions,
87            has_next,
88            subscribed,
89            incremental,
90            created_at,
91        }
92    }
93
94    /// If path is None, this is a primary response.
95    pub fn is_primary(&self) -> bool {
96        self.path.is_none()
97    }
98
99    /// append_errors default the errors `path` with the one provided.
100    pub fn append_errors(&mut self, errors: &mut Vec<Error>) {
101        self.errors.append(errors)
102    }
103
104    /// Create a [`Response`] from the supplied [`Bytes`].
105    ///
106    /// This will return an error (identifying the faulty service) if the input is invalid.
107    pub(crate) fn from_bytes(b: Bytes) -> Result<Response, MalformedResponseError> {
108        let value = Value::from_bytes(b).map_err(|error| {
109            let mut reason = error.to_string();
110
111            // RFC 8259 ยง7 requires that non-BMP characters encoded as \uXXXX escapes use
112            // a surrogate pair: a high surrogate (\uD800โ€“\uDBFF) immediately followed by a
113            // low surrogate (\uDC00โ€“\uDFFF). A lone high surrogate is invalid JSON.
114            // https://www.rfc-editor.org/rfc/rfc8259#section-7
115            //
116            // In serde_json, `UnexpectedEndOfHexEscape` is only reachable from the
117            // surrogate-parsing code path, so this message string uniquely identifies a
118            // lone-surrogate error โ€” no additional byte-level check is needed.
119            if error.classify() == serde_json::error::Category::Syntax
120                && reason.contains("unexpected end of hex escape")
121            {
122                reason.push_str("; the response contains an unpaired Unicode surrogate");
123            }
124            MalformedResponseError { reason }
125        })?;
126        Response::from_value(value)
127    }
128
129    pub(crate) fn from_value(value: Value) -> Result<Response, MalformedResponseError> {
130        let mut object = ensure_object!(value).map_err(|error| MalformedResponseError {
131            reason: error.to_string(),
132        })?;
133        let data = object.remove("data");
134        let errors = extract_key_value_from_object!(object, "errors", Value::Array(v) => v)
135            .map_err(|err| MalformedResponseError {
136                reason: err.to_string(),
137            })?
138            .into_iter()
139            .flatten()
140            .map(Error::from_value)
141            .collect::<Result<Vec<Error>, MalformedResponseError>>()?;
142        let extensions =
143            extract_key_value_from_object!(object, "extensions", Value::Object(o) => o)
144                .map_err(|err| MalformedResponseError {
145                    reason: err.to_string(),
146                })?
147                .unwrap_or_default();
148        let label = extract_key_value_from_object!(object, "label", Value::String(s) => s)
149            .map_err(|err| MalformedResponseError {
150                reason: err.to_string(),
151            })?
152            .map(|s| s.as_str().to_string());
153        let path = extract_key_value_from_object!(object, "path")
154            .map(serde_json_bytes::from_value)
155            .transpose()
156            .map_err(|err| MalformedResponseError {
157                reason: err.to_string(),
158            })?;
159        let has_next = extract_key_value_from_object!(object, "hasNext", Value::Bool(b) => b)
160            .map_err(|err| MalformedResponseError {
161                reason: err.to_string(),
162            })?;
163        let incremental =
164            extract_key_value_from_object!(object, "incremental", Value::Array(a) => a).map_err(
165                |err| MalformedResponseError {
166                    reason: err.to_string(),
167                },
168            )?;
169        let incremental: Vec<IncrementalResponse> = match incremental {
170            Some(v) => v
171                .into_iter()
172                .map(serde_json_bytes::from_value)
173                .collect::<Result<Vec<IncrementalResponse>, _>>()
174                .map_err(|err| MalformedResponseError {
175                    reason: err.to_string(),
176                })?,
177            None => vec![],
178        };
179        // Graphql spec says:
180        // If the data entry in the response is not present, the errors entry in the response must not be empty.
181        // It must contain at least one error. The errors it contains should indicate why no data was able to be returned.
182        if data.is_none() && errors.is_empty() {
183            return Err(MalformedResponseError {
184                reason: "graphql response without data must contain at least one error".to_string(),
185            });
186        }
187
188        Ok(Response {
189            label,
190            data,
191            path,
192            errors,
193            extensions,
194            has_next,
195            subscribed: None,
196            incremental,
197            created_at: None,
198        })
199    }
200}
201
202/// A graphql incremental response.
203/// Used with `@defer`
204#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
205#[serde(rename_all = "camelCase")]
206#[non_exhaustive]
207pub struct IncrementalResponse {
208    /// The label that was passed to the defer or stream directive for this patch.
209    #[serde(skip_serializing_if = "Option::is_none", default)]
210    pub label: Option<String>,
211
212    /// The response data.
213    #[serde(skip_serializing_if = "Option::is_none", default)]
214    pub data: Option<Value>,
215
216    /// The path that the data should be merged at.
217    #[serde(skip_serializing_if = "Option::is_none", default)]
218    pub path: Option<Path>,
219
220    /// The optional graphql errors encountered.
221    #[serde(skip_serializing_if = "Vec::is_empty", default)]
222    pub errors: Vec<Error>,
223
224    /// The optional graphql extensions.
225    #[serde(skip_serializing_if = "Object::is_empty", default)]
226    pub extensions: Object,
227}
228
229#[buildstructor::buildstructor]
230impl IncrementalResponse {
231    /// Constructor
232    #[builder(visibility = "pub")]
233    fn new(
234        label: Option<String>,
235        data: Option<Value>,
236        path: Option<Path>,
237        errors: Vec<Error>,
238        extensions: Map<ByteString, Value>,
239    ) -> Self {
240        Self {
241            label,
242            data,
243            path,
244            errors,
245            extensions,
246        }
247    }
248
249    /// append_errors default the errors `path` with the one provided.
250    pub fn append_errors(&mut self, errors: &mut Vec<Error>) {
251        self.errors.append(errors)
252    }
253}
254
255impl From<ExecutionResponse> for Response {
256    fn from(response: ExecutionResponse) -> Response {
257        let ExecutionResponse { errors, data } = response;
258        Self {
259            errors: errors.into_graphql_errors().unwrap(),
260            data: data.map(serde_json_bytes::Value::Object),
261            extensions: Default::default(),
262            label: None,
263            path: None,
264            has_next: None,
265            subscribed: None,
266            created_at: None,
267            incremental: Vec::new(),
268        }
269    }
270}
271
272#[cfg(test)]
273impl Response {
274    pub(crate) fn errors_with_code<'a>(&'a self, code: &'a str) -> impl Iterator<Item = &'a Error> {
275        self.errors
276            .iter()
277            .filter(move |err| err.extension_code().is_some_and(|c| c == code))
278    }
279
280    pub(crate) fn contains_error_code(&self, code: &str) -> bool {
281        self.errors_with_code(code).next().is_some()
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use serde_json::json;
288    use serde_json_bytes::json as bjson;
289    use uuid::Uuid;
290
291    use super::*;
292    use crate::assert_response_eq_ignoring_error_id;
293    use crate::graphql;
294    use crate::graphql::Error;
295    use crate::graphql::Location;
296    use crate::graphql::Response;
297
298    #[test]
299    fn test_append_errors_path_fallback_and_override() {
300        let uuid1 = Uuid::new_v4();
301        let uuid2 = Uuid::new_v4();
302        let expected_errors = vec![
303            Error::builder()
304                .message("Something terrible happened!")
305                .path(Path::from("here"))
306                .apollo_id(uuid1)
307                .build(),
308            Error::builder()
309                .message("I mean for real")
310                .apollo_id(uuid2)
311                .build(),
312        ];
313
314        let mut errors_to_append = vec![
315            Error::builder()
316                .message("Something terrible happened!")
317                .path(Path::from("here"))
318                .apollo_id(uuid1)
319                .build(),
320            Error::builder()
321                .message("I mean for real")
322                .apollo_id(uuid2)
323                .build(),
324        ];
325
326        let mut response = Response::builder().build();
327        response.append_errors(&mut errors_to_append);
328        assert_eq!(response.errors, expected_errors);
329    }
330
331    #[test]
332    fn test_response() {
333        let result = serde_json::from_str::<Response>(
334            json!(
335            {
336              "errors": [
337                {
338                  "message": "Name for character with ID 1002 could not be fetched.",
339                  "locations": [{ "line": 6, "column": 7 }],
340                  "path": ["hero", "heroFriends", 1, "name"],
341                  "extensions": {
342                    "error-extension": 5,
343                  }
344                }
345              ],
346              "data": {
347                "hero": {
348                  "name": "R2-D2",
349                  "heroFriends": [
350                    {
351                      "id": "1000",
352                      "name": "Luke Skywalker"
353                    },
354                    {
355                      "id": "1002",
356                      "name": null
357                    },
358                    {
359                      "id": "1003",
360                      "name": "Leia Organa"
361                    }
362                  ]
363                }
364              },
365              "extensions": {
366                "response-extension": 3,
367              }
368            })
369            .to_string()
370            .as_str(),
371        );
372        let response = result.unwrap();
373        assert_response_eq_ignoring_error_id!(
374            response,
375            Response::builder()
376                .data(json!({
377                  "hero": {
378                    "name": "R2-D2",
379                    "heroFriends": [
380                      {
381                        "id": "1000",
382                        "name": "Luke Skywalker"
383                      },
384                      {
385                        "id": "1002",
386                        "name": null
387                      },
388                      {
389                        "id": "1003",
390                        "name": "Leia Organa"
391                      }
392                    ]
393                  }
394                }))
395                .errors(vec![
396                    Error::builder()
397                        .message("Name for character with ID 1002 could not be fetched.")
398                        .locations(vec!(Location { line: 6, column: 7 }))
399                        .path(Path::from("hero/heroFriends/1/name"))
400                        .extensions(
401                            bjson!({ "error-extension": 5, })
402                                .as_object()
403                                .cloned()
404                                .unwrap()
405                        )
406                        .build()
407                ])
408                .extensions(
409                    bjson!({
410                        "response-extension": 3,
411                    })
412                    .as_object()
413                    .cloned()
414                    .unwrap()
415                )
416                .build()
417        );
418    }
419
420    #[test]
421    fn test_patch_response() {
422        let result = serde_json::from_str::<Response>(
423            json!(
424            {
425              "label": "part",
426              "hasNext": true,
427              "path": ["hero", "heroFriends", 1, "name"],
428              "errors": [
429                {
430                  "message": "Name for character with ID 1002 could not be fetched.",
431                  "locations": [{ "line": 6, "column": 7 }],
432                  "path": ["hero", "heroFriends", 1, "name"],
433                  "extensions": {
434                    "error-extension": 5,
435                  }
436                }
437              ],
438              "data": {
439                "hero": {
440                  "name": "R2-D2",
441                  "heroFriends": [
442                    {
443                      "id": "1000",
444                      "name": "Luke Skywalker"
445                    },
446                    {
447                      "id": "1002",
448                      "name": null
449                    },
450                    {
451                      "id": "1003",
452                      "name": "Leia Organa"
453                    }
454                  ]
455                }
456              },
457              "extensions": {
458                "response-extension": 3,
459              }
460            })
461            .to_string()
462            .as_str(),
463        );
464        let response = result.unwrap();
465        assert_response_eq_ignoring_error_id!(
466            response,
467            Response::builder()
468                .label("part".to_owned())
469                .data(json!({
470                  "hero": {
471                    "name": "R2-D2",
472                    "heroFriends": [
473                      {
474                        "id": "1000",
475                        "name": "Luke Skywalker"
476                      },
477                      {
478                        "id": "1002",
479                        "name": null
480                      },
481                      {
482                        "id": "1003",
483                        "name": "Leia Organa"
484                      }
485                    ]
486                  }
487                }))
488                .path(Path::from("hero/heroFriends/1/name"))
489                .errors(vec![
490                    Error::builder()
491                        .message("Name for character with ID 1002 could not be fetched.")
492                        .locations(vec!(Location { line: 6, column: 7 }))
493                        .path(Path::from("hero/heroFriends/1/name"))
494                        .extensions(
495                            bjson!({ "error-extension": 5, })
496                                .as_object()
497                                .cloned()
498                                .unwrap()
499                        )
500                        .build()
501                ])
502                .extensions(
503                    bjson!({
504                        "response-extension": 3,
505                    })
506                    .as_object()
507                    .cloned()
508                    .unwrap()
509                )
510                .has_next(true)
511                .build()
512        );
513    }
514
515    #[test]
516    fn test_no_data_and_no_errors() {
517        let response = Response::from_bytes("{\"errors\":null}".into());
518        assert_eq!(
519            response.expect_err("no data and no errors"),
520            MalformedResponseError {
521                reason: "graphql response without data must contain at least one error".to_string(),
522            }
523        );
524    }
525
526    #[test]
527    fn test_data_null() {
528        let response = Response::from_bytes("{\"data\":null}".into()).unwrap();
529        assert_eq!(
530            response,
531            Response::builder().data(Some(Value::Null)).build(),
532        );
533    }
534
535    /// Tests for Unicode / emoji handling in subgraph responses.
536    ///
537    /// Non-BMP characters (U+10000 and above, e.g. ๐Ÿ’ฐ U+1F4B0) require two UTF-16 code units
538    /// when encoded as \uXXXX JSON escapes: a high surrogate (\uD800โ€“\uDBFF) followed immediately
539    /// by a low surrogate (\uDC00โ€“\uDFFF). serde_json enforces this strictly; a lone high
540    /// surrogate is rejected as malformed JSON (RFC 8259 ยง7).
541    mod unicode {
542        use rstest::rstest;
543
544        use super::*;
545
546        // Valid encodings โ€” all should parse successfully and round-trip to the same data.
547        #[rstest]
548        // Raw UTF-8 bytes: the most common and correct encoding.
549        #[case::raw_utf8("{ \"data\": { \"greeting\": \"hello ๐Ÿ’ฐ๐Ÿ’•\" } }", bjson!({ "greeting": "hello ๐Ÿ’ฐ๐Ÿ’•" }))]
550        // \uD83D\uDCB0 = ๐Ÿ’ฐ, \uD83D\uDC95 = ๐Ÿ’•: valid surrogate pairs as emitted by Java's Jackson
551        // (ensure_ascii=true) or Python's json.dumps(ensure_ascii=True).
552        #[case::surrogate_pairs(r#"{"data":{"greeting":"hello \uD83D\uDCB0\uD83D\uDC95"}}"#, bjson!({ "greeting": "hello ๐Ÿ’ฐ๐Ÿ’•" }))]
553        // โค is U+2764 (BMP, single \uXXXX); ๐Ÿ˜€ is U+1F600 (non-BMP, surrogate pair \uD83D\uDE00).
554        #[case::bmp_and_surrogate_pair(r#"{"data":{"greeting":"\u2764 \uD83D\uDE00"}}"#, bjson!({ "greeting": "โค ๐Ÿ˜€" }))]
555        fn valid_emoji(#[case] json: &str, #[case] expected: Value) {
556            let resp = Response::from_bytes(Bytes::copy_from_slice(json.as_bytes())).unwrap();
557            assert_eq!(resp.data, Some(expected));
558        }
559
560        // Invalid encodings โ€” lone high surrogates must be rejected with a helpful hint.
561        #[rstest]
562        // \uD83D followed by a space: high surrogate with no following \uDCxx (first serde_json branch).
563        #[case::lone_surrogate_space(r#"{"data":{"greeting":"hello \uD83D end"}}"#)]
564        // \uD83D\n: high surrogate followed by a valid escape that isn't \u (second branch).
565        #[case::lone_surrogate_non_u_escape(r#"{"data":{"greeting":"hello \uD83D\n end"}}"#)]
566        fn lone_surrogate_rejected(#[case] json: &str) {
567            let err = Response::from_bytes(Bytes::copy_from_slice(json.as_bytes())).unwrap_err();
568            assert!(
569                err.reason.contains("unexpected end of hex escape"),
570                "expected base serde_json error, got: {err}"
571            );
572            assert!(
573                err.reason.contains("unpaired Unicode surrogate"),
574                "expected surrogate hint in error, got: {err}"
575            );
576        }
577    }
578}