Skip to content

Commit c9f3415

Browse files
fix(executor): handle subgraph errors with extensions correctly (#494)
Ref ROUTER-157 When subgraphs returns an error with extensions but without `data`, the deserialization was breaking due to the issue -> cloudwego/sonic-rs#114 A manual deserialization is needed in this case --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 61b3b63 commit c9f3415

File tree

2 files changed

+96
-4
lines changed

2 files changed

+96
-4
lines changed

lib/executor/src/response/graphql_error.rs

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
use core::fmt;
12
use graphql_parser::Pos;
23
use graphql_tools::validation::utils::ValidationError;
3-
use serde::{Deserialize, Serialize};
4+
use serde::{de, Deserialize, Deserializer, Serialize};
45
use sonic_rs::Value;
56
use std::collections::HashMap;
67

@@ -177,17 +178,74 @@ impl GraphQLErrorPath {
177178
}
178179
}
179180

180-
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
181+
#[derive(Clone, Debug, Serialize, Default)]
181182
#[serde(rename_all = "camelCase")]
182183
pub struct GraphQLErrorExtensions {
183-
#[serde(default, skip_serializing_if = "Option::is_none")]
184+
#[serde(skip_serializing_if = "Option::is_none")]
184185
pub code: Option<String>,
185-
#[serde(default, skip_serializing_if = "Option::is_none")]
186+
#[serde(skip_serializing_if = "Option::is_none")]
186187
pub service_name: Option<String>,
187188
#[serde(flatten)]
188189
pub extensions: HashMap<String, Value>,
189190
}
190191

192+
// Workaround for https://github.com/cloudwego/sonic-rs/issues/114
193+
194+
impl<'de> Deserialize<'de> for GraphQLErrorExtensions {
195+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
196+
where
197+
D: Deserializer<'de>,
198+
{
199+
struct GraphQLErrorExtensionsVisitor;
200+
201+
impl<'de> de::Visitor<'de> for GraphQLErrorExtensionsVisitor {
202+
type Value = GraphQLErrorExtensions;
203+
204+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
205+
formatter.write_str("a map for GraphQLErrorExtensions")
206+
}
207+
208+
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
209+
where
210+
A: de::MapAccess<'de>,
211+
{
212+
let mut code = None;
213+
let mut service_name = None;
214+
let mut extensions = HashMap::new();
215+
216+
while let Some(key) = map.next_key::<String>()? {
217+
match key.as_str() {
218+
"code" => {
219+
if code.is_some() {
220+
return Err(de::Error::duplicate_field("code"));
221+
}
222+
code = Some(map.next_value()?);
223+
}
224+
"serviceName" => {
225+
if service_name.is_some() {
226+
return Err(de::Error::duplicate_field("serviceName"));
227+
}
228+
service_name = Some(map.next_value()?);
229+
}
230+
other_key => {
231+
let value: Value = map.next_value()?;
232+
extensions.insert(other_key.to_string(), value);
233+
}
234+
}
235+
}
236+
237+
Ok(GraphQLErrorExtensions {
238+
code,
239+
service_name,
240+
extensions,
241+
})
242+
}
243+
}
244+
245+
deserializer.deserialize_map(GraphQLErrorExtensionsVisitor)
246+
}
247+
}
248+
191249
impl GraphQLErrorExtensions {
192250
pub fn new_from_code(code: &str) -> Self {
193251
GraphQLErrorExtensions {

lib/executor/src/response/subgraph_response.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,37 @@ impl<'de> de::Deserialize<'de> for SubgraphResponse<'de> {
8080
})
8181
}
8282
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
// When subgraph returns an error with custom extensions but without `data` field
87+
#[test]
88+
fn deserialize_response_without_data_with_errors_with_extensions() {
89+
let json_response = r#"
90+
{
91+
"errors": [
92+
{
93+
"message": "Random error from subgraph",
94+
"extensions":{
95+
"statusCode": 400
96+
}
97+
}
98+
]
99+
}"#;
100+
101+
let response: super::SubgraphResponse =
102+
sonic_rs::from_str(json_response).expect("Failed to deserialize");
103+
104+
assert!(response.data.is_null());
105+
let errors = response.errors.as_ref().unwrap();
106+
insta::assert_snapshot!(sonic_rs::to_string_pretty(&errors).unwrap(), @r###"
107+
[
108+
{
109+
"message": "Random error from subgraph",
110+
"extensions": {
111+
"statusCode": 400
112+
}
113+
}
114+
]"###);
115+
}
116+
}

0 commit comments

Comments
 (0)