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
272impl Response {
273    pub(crate) fn all_errors(&self) -> impl Iterator<Item = &Error> {
274        self.errors
275            .iter()
276            .chain(self.incremental.iter().flat_map(|r| r.errors.iter()))
277    }
278
279    pub(crate) fn contains_errors(&self) -> bool {
280        self.all_errors().next().is_some()
281    }
282}
283
284#[cfg(test)]
285impl Response {
286    pub(crate) fn errors_with_code<'a>(&'a self, code: &'a str) -> impl Iterator<Item = &'a Error> {
287        self.errors
288            .iter()
289            .filter(move |err| err.extension_code().is_some_and(|c| c == code))
290    }
291
292    pub(crate) fn contains_error_code(&self, code: &str) -> bool {
293        self.errors_with_code(code).next().is_some()
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use serde_json::json;
300    use serde_json_bytes::json as bjson;
301    use uuid::Uuid;
302
303    use super::*;
304    use crate::assert_response_eq_ignoring_error_id;
305    use crate::graphql;
306    use crate::graphql::Error;
307    use crate::graphql::Location;
308    use crate::graphql::Response;
309
310    #[test]
311    fn test_append_errors_path_fallback_and_override() {
312        let uuid1 = Uuid::new_v4();
313        let uuid2 = Uuid::new_v4();
314        let expected_errors = 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 errors_to_append = vec![
327            Error::builder()
328                .message("Something terrible happened!")
329                .path(Path::from("here"))
330                .apollo_id(uuid1)
331                .build(),
332            Error::builder()
333                .message("I mean for real")
334                .apollo_id(uuid2)
335                .build(),
336        ];
337
338        let mut response = Response::builder().build();
339        response.append_errors(&mut errors_to_append);
340        assert_eq!(response.errors, expected_errors);
341    }
342
343    #[test]
344    fn test_response() {
345        let result = serde_json::from_str::<Response>(
346            json!(
347            {
348              "errors": [
349                {
350                  "message": "Name for character with ID 1002 could not be fetched.",
351                  "locations": [{ "line": 6, "column": 7 }],
352                  "path": ["hero", "heroFriends", 1, "name"],
353                  "extensions": {
354                    "error-extension": 5,
355                  }
356                }
357              ],
358              "data": {
359                "hero": {
360                  "name": "R2-D2",
361                  "heroFriends": [
362                    {
363                      "id": "1000",
364                      "name": "Luke Skywalker"
365                    },
366                    {
367                      "id": "1002",
368                      "name": null
369                    },
370                    {
371                      "id": "1003",
372                      "name": "Leia Organa"
373                    }
374                  ]
375                }
376              },
377              "extensions": {
378                "response-extension": 3,
379              }
380            })
381            .to_string()
382            .as_str(),
383        );
384        let response = result.unwrap();
385        assert_response_eq_ignoring_error_id!(
386            response,
387            Response::builder()
388                .data(json!({
389                  "hero": {
390                    "name": "R2-D2",
391                    "heroFriends": [
392                      {
393                        "id": "1000",
394                        "name": "Luke Skywalker"
395                      },
396                      {
397                        "id": "1002",
398                        "name": null
399                      },
400                      {
401                        "id": "1003",
402                        "name": "Leia Organa"
403                      }
404                    ]
405                  }
406                }))
407                .errors(vec![
408                    Error::builder()
409                        .message("Name for character with ID 1002 could not be fetched.")
410                        .locations(vec!(Location { line: 6, column: 7 }))
411                        .path(Path::from("hero/heroFriends/1/name"))
412                        .extensions(
413                            bjson!({ "error-extension": 5, })
414                                .as_object()
415                                .cloned()
416                                .unwrap()
417                        )
418                        .build()
419                ])
420                .extensions(
421                    bjson!({
422                        "response-extension": 3,
423                    })
424                    .as_object()
425                    .cloned()
426                    .unwrap()
427                )
428                .build()
429        );
430    }
431
432    #[test]
433    fn test_patch_response() {
434        let result = serde_json::from_str::<Response>(
435            json!(
436            {
437              "label": "part",
438              "hasNext": true,
439              "path": ["hero", "heroFriends", 1, "name"],
440              "errors": [
441                {
442                  "message": "Name for character with ID 1002 could not be fetched.",
443                  "locations": [{ "line": 6, "column": 7 }],
444                  "path": ["hero", "heroFriends", 1, "name"],
445                  "extensions": {
446                    "error-extension": 5,
447                  }
448                }
449              ],
450              "data": {
451                "hero": {
452                  "name": "R2-D2",
453                  "heroFriends": [
454                    {
455                      "id": "1000",
456                      "name": "Luke Skywalker"
457                    },
458                    {
459                      "id": "1002",
460                      "name": null
461                    },
462                    {
463                      "id": "1003",
464                      "name": "Leia Organa"
465                    }
466                  ]
467                }
468              },
469              "extensions": {
470                "response-extension": 3,
471              }
472            })
473            .to_string()
474            .as_str(),
475        );
476        let response = result.unwrap();
477        assert_response_eq_ignoring_error_id!(
478            response,
479            Response::builder()
480                .label("part".to_owned())
481                .data(json!({
482                  "hero": {
483                    "name": "R2-D2",
484                    "heroFriends": [
485                      {
486                        "id": "1000",
487                        "name": "Luke Skywalker"
488                      },
489                      {
490                        "id": "1002",
491                        "name": null
492                      },
493                      {
494                        "id": "1003",
495                        "name": "Leia Organa"
496                      }
497                    ]
498                  }
499                }))
500                .path(Path::from("hero/heroFriends/1/name"))
501                .errors(vec![
502                    Error::builder()
503                        .message("Name for character with ID 1002 could not be fetched.")
504                        .locations(vec!(Location { line: 6, column: 7 }))
505                        .path(Path::from("hero/heroFriends/1/name"))
506                        .extensions(
507                            bjson!({ "error-extension": 5, })
508                                .as_object()
509                                .cloned()
510                                .unwrap()
511                        )
512                        .build()
513                ])
514                .extensions(
515                    bjson!({
516                        "response-extension": 3,
517                    })
518                    .as_object()
519                    .cloned()
520                    .unwrap()
521                )
522                .has_next(true)
523                .build()
524        );
525    }
526
527    #[test]
528    fn test_no_data_and_no_errors() {
529        let response = Response::from_bytes("{\"errors\":null}".into());
530        assert_eq!(
531            response.expect_err("no data and no errors"),
532            MalformedResponseError {
533                reason: "graphql response without data must contain at least one error".to_string(),
534            }
535        );
536    }
537
538    #[test]
539    fn test_data_null() {
540        let response = Response::from_bytes("{\"data\":null}".into()).unwrap();
541        assert_eq!(
542            response,
543            Response::builder().data(Some(Value::Null)).build(),
544        );
545    }
546
547    /// Tests for Unicode / emoji handling in subgraph responses.
548    ///
549    /// Non-BMP characters (U+10000 and above, e.g. ๐Ÿ’ฐ U+1F4B0) require two UTF-16 code units
550    /// when encoded as \uXXXX JSON escapes: a high surrogate (\uD800โ€“\uDBFF) followed immediately
551    /// by a low surrogate (\uDC00โ€“\uDFFF). serde_json enforces this strictly; a lone high
552    /// surrogate is rejected as malformed JSON (RFC 8259 ยง7).
553    mod unicode {
554        use rstest::rstest;
555
556        use super::*;
557
558        // Valid encodings โ€” all should parse successfully and round-trip to the same data.
559        #[rstest]
560        // Raw UTF-8 bytes: the most common and correct encoding.
561        #[case::raw_utf8("{ \"data\": { \"greeting\": \"hello ๐Ÿ’ฐ๐Ÿ’•\" } }", bjson!({ "greeting": "hello ๐Ÿ’ฐ๐Ÿ’•" }))]
562        // \uD83D\uDCB0 = ๐Ÿ’ฐ, \uD83D\uDC95 = ๐Ÿ’•: valid surrogate pairs as emitted by Java's Jackson
563        // (ensure_ascii=true) or Python's json.dumps(ensure_ascii=True).
564        #[case::surrogate_pairs(r#"{"data":{"greeting":"hello \uD83D\uDCB0\uD83D\uDC95"}}"#, bjson!({ "greeting": "hello ๐Ÿ’ฐ๐Ÿ’•" }))]
565        // โค is U+2764 (BMP, single \uXXXX); ๐Ÿ˜€ is U+1F600 (non-BMP, surrogate pair \uD83D\uDE00).
566        #[case::bmp_and_surrogate_pair(r#"{"data":{"greeting":"\u2764 \uD83D\uDE00"}}"#, bjson!({ "greeting": "โค ๐Ÿ˜€" }))]
567        fn valid_emoji(#[case] json: &str, #[case] expected: Value) {
568            let resp = Response::from_bytes(Bytes::copy_from_slice(json.as_bytes())).unwrap();
569            assert_eq!(resp.data, Some(expected));
570        }
571
572        // Invalid encodings โ€” lone high surrogates must be rejected with a helpful hint.
573        #[rstest]
574        // \uD83D followed by a space: high surrogate with no following \uDCxx (first serde_json branch).
575        #[case::lone_surrogate_space(r#"{"data":{"greeting":"hello \uD83D end"}}"#)]
576        // \uD83D\n: high surrogate followed by a valid escape that isn't \u (second branch).
577        #[case::lone_surrogate_non_u_escape(r#"{"data":{"greeting":"hello \uD83D\n end"}}"#)]
578        fn lone_surrogate_rejected(#[case] json: &str) {
579            let err = Response::from_bytes(Bytes::copy_from_slice(json.as_bytes())).unwrap_err();
580            assert!(
581                err.reason.contains("unexpected end of hex escape"),
582                "expected base serde_json error, got: {err}"
583            );
584            assert!(
585                err.reason.contains("unpaired Unicode surrogate"),
586                "expected surrogate hint in error, got: {err}"
587            );
588        }
589    }
590}