Skip to content

Commit f4557a4

Browse files
authored
Handle empty responses from subgraphs and failed entity calls (#500)
This PR tackles an issue where the subgraph error could end up being omitted entirely in the client response. It now also handles cases where the upstream response is empty. In cases where a subgraph call fails, there's no point to clone the error for every entity representation. It'd result in a bloated response with the same thing repeated multiple times (imagine fetching 1000s representations). Instead of cloning and adding `path` property to associate the error with a point in GraphQL document, we attach `affectedPath` that represents the Flatten(path) where the error occurred.
1 parent c9f3415 commit f4557a4

File tree

4 files changed

+52
-14
lines changed

4 files changed

+52
-14
lines changed

lib/executor/src/context.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,24 @@ impl<'a> ExecutionContext<'a> {
4545
pub fn handle_errors(
4646
&mut self,
4747
subgraph_name: &str,
48+
affected_path: Option<String>,
4849
errors: Option<Vec<GraphQLError>>,
4950
entity_index_error_map: Option<HashMap<&usize, Vec<GraphQLErrorPath>>>,
5051
) {
5152
if let Some(response_errors) = errors {
5253
for response_error in response_errors {
53-
let response_error_with_subgraph_name =
54-
response_error.add_subgraph_name(subgraph_name);
54+
let mut processed_error = response_error.add_subgraph_name(subgraph_name);
55+
56+
if let Some(path) = &affected_path {
57+
processed_error = processed_error.add_affected_path(path.clone());
58+
}
59+
5560
if let Some(entity_index_error_map) = &entity_index_error_map {
56-
let normalized_errors = response_error_with_subgraph_name
57-
.normalize_entity_error(entity_index_error_map);
61+
let normalized_errors =
62+
processed_error.normalize_entity_error(entity_index_error_map);
5863
self.errors.extend(normalized_errors);
5964
} else {
60-
self.errors.push(response_error_with_subgraph_name);
65+
self.errors.push(processed_error);
6166
}
6267
}
6368
}

lib/executor/src/execution/plan.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ impl<'exec> Executor<'exec> {
509509
job.response.body,
510510
job.fetch_node_id,
511511
) {
512-
ctx.handle_errors(&job.subgraph_name, response.errors, None);
512+
ctx.handle_errors(&job.subgraph_name, None, response.errors, None);
513513
if let Some(output_rewrites) = output_rewrites {
514514
for output_rewrite in output_rewrites {
515515
output_rewrite
@@ -588,9 +588,18 @@ impl<'exec> Executor<'exec> {
588588
);
589589
ctx.handle_errors(
590590
&job.subgraph_name,
591+
Some(job.flatten_node_path.to_string()),
591592
response.errors,
592593
entity_index_error_map,
593594
);
595+
} else if let Some(errors) = response.errors {
596+
// No entities were returned, but there are errors to handle.
597+
// We associate them with the flattened path and subgraph.
598+
let affected_path = job.flatten_node_path.to_string();
599+
ctx.errors.extend(errors.into_iter().map(|e| {
600+
e.add_subgraph_name(&job.subgraph_name)
601+
.add_affected_path(affected_path.clone())
602+
}));
594603
}
595604
}
596605
}
@@ -902,6 +911,7 @@ mod tests {
902911
}];
903912
ctx.handle_errors(
904913
"subgraph_a",
914+
None,
905915
Some(response_errors),
906916
Some(entity_index_error_map),
907917
);

lib/executor/src/executors/http.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,24 @@ impl HTTPSubgraphExecutor {
162162
);
163163

164164
let (parts, body) = res.into_parts();
165+
let body = body
166+
.collect()
167+
.await
168+
.map_err(|e| {
169+
SubgraphExecutorError::RequestFailure(self.endpoint.to_string(), e.to_string())
170+
})?
171+
.to_bytes();
172+
173+
if body.is_empty() {
174+
return Err(SubgraphExecutorError::RequestFailure(
175+
self.endpoint.to_string(),
176+
"Empty response body".to_string(),
177+
));
178+
}
165179

166180
Ok(SharedResponse {
167181
status: parts.status,
168-
body: body
169-
.collect()
170-
.await
171-
.map_err(|e| {
172-
SubgraphExecutorError::RequestFailure(self.endpoint.to_string(), e.to_string())
173-
})?
174-
.to_bytes(),
182+
body,
175183
headers: parts.headers,
176184
})
177185
}

lib/executor/src/response/graphql_error.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ impl GraphQLError {
113113
.get_or_insert("DOWNSTREAM_SERVICE_ERROR".to_string());
114114
self
115115
}
116+
117+
pub fn add_affected_path(mut self, affected_path: String) -> Self {
118+
self.extensions.affected_path = Some(affected_path);
119+
self
120+
}
116121
}
117122

118123
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -185,6 +190,9 @@ pub struct GraphQLErrorExtensions {
185190
pub code: Option<String>,
186191
#[serde(skip_serializing_if = "Option::is_none")]
187192
pub service_name: Option<String>,
193+
/// Corresponds to a path of a Flatten(Fetch) node that caused the error.
194+
#[serde(default, skip_serializing_if = "Option::is_none")]
195+
pub affected_path: Option<String>,
188196
#[serde(flatten)]
189197
pub extensions: HashMap<String, Value>,
190198
}
@@ -251,16 +259,20 @@ impl GraphQLErrorExtensions {
251259
GraphQLErrorExtensions {
252260
code: Some(code.to_string()),
253261
service_name: None,
262+
affected_path: None,
254263
extensions: HashMap::new(),
255264
}
256265
}
266+
257267
pub fn new_from_code_and_service_name(code: &str, service_name: &str) -> Self {
258268
GraphQLErrorExtensions {
259269
code: Some(code.to_string()),
260270
service_name: Some(service_name.to_string()),
271+
affected_path: None,
261272
extensions: HashMap::new(),
262273
}
263274
}
275+
264276
pub fn get(&self, key: &str) -> Option<&Value> {
265277
self.extensions.get(key)
266278
}
@@ -270,6 +282,9 @@ impl GraphQLErrorExtensions {
270282
}
271283

272284
pub fn is_empty(&self) -> bool {
273-
self.code.is_none() && self.service_name.is_none() && self.extensions.is_empty()
285+
self.code.is_none()
286+
&& self.service_name.is_none()
287+
&& self.affected_path.is_none()
288+
&& self.extensions.is_empty()
274289
}
275290
}

0 commit comments

Comments
 (0)