Skip to content

Commit 388afab

Browse files
coprocessor: fix parsing of graphql responses with null as data (#7141)
1 parent c2a2801 commit 388afab

File tree

13 files changed

+377
-352
lines changed

13 files changed

+377
-352
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
### Fix Parsing of Coprocessor GraphQL Responses ([PR #7141](https://github.com/apollographql/router/pull/7141))
2+
3+
Previously Router ignored `data: null` property inside GraphQL response returned by coprocessor.
4+
According to [GraphQL Spectification](https://spec.graphql.org/draft/#sel-FAPHLJCAACEBxlY):
5+
6+
> If an error was raised during the execution that prevented a valid response, the "data" entry in the response should be null.
7+
8+
That means if coprocessor returned valid execution error, for example:
9+
10+
```json
11+
{
12+
"data": null,
13+
"errors": [{ "message": "Some execution error" }]
14+
}
15+
```
16+
17+
Router violated above restriction from GraphQL Specification by returning following response to client:
18+
19+
```json
20+
{
21+
"errors": [{ "message": "Some execution error" }]
22+
}
23+
```
24+
25+
This fix ensures full compliance with the GraphQL specification by preserving the complete structure of error responses from coprocessors.
26+
27+
Contributed by [@IvanGoncharov](https://github.com/IvanGoncharov) in [#7141](https://github.com/apollographql/router/pull/7141)

apollo-router/src/graphql/mod.rs

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use futures::Stream;
1313
use heck::ToShoutySnakeCase;
1414
pub use request::Request;
1515
pub use response::IncrementalResponse;
16+
use response::MalformedResponseError;
1617
pub use response::Response;
1718
use serde::Deserialize;
1819
use serde::Serialize;
@@ -21,7 +22,6 @@ use serde_json_bytes::Map as JsonMap;
2122
use serde_json_bytes::Value;
2223
pub(crate) use visitor::ResponseVisitor;
2324

24-
use crate::error::FetchError;
2525
use crate::json_ext::Object;
2626
use crate::json_ext::Path;
2727
pub use crate::json_ext::Path as JsonPath;
@@ -125,41 +125,39 @@ impl Error {
125125
}
126126
}
127127

128-
pub(crate) fn from_value(service_name: &str, value: Value) -> Result<Error, FetchError> {
129-
let mut object =
130-
ensure_object!(value).map_err(|error| FetchError::SubrequestMalformedResponse {
131-
service: service_name.to_string(),
132-
reason: format!("invalid error within `errors`: {}", error),
133-
})?;
128+
pub(crate) fn from_value(value: Value) -> Result<Error, MalformedResponseError> {
129+
let mut object = ensure_object!(value).map_err(|error| MalformedResponseError {
130+
reason: format!("invalid error within `errors`: {}", error),
131+
})?;
134132

135133
let extensions =
136134
extract_key_value_from_object!(object, "extensions", Value::Object(o) => o)
137-
.map_err(|err| FetchError::SubrequestMalformedResponse {
138-
service: service_name.to_string(),
135+
.map_err(|err| MalformedResponseError {
139136
reason: format!("invalid `extensions` within error: {}", err),
140137
})?
141138
.unwrap_or_default();
142-
let message = extract_key_value_from_object!(object, "message", Value::String(s) => s)
143-
.map_err(|err| FetchError::SubrequestMalformedResponse {
144-
service: service_name.to_string(),
139+
let message = match extract_key_value_from_object!(object, "message", Value::String(s) => s)
140+
{
141+
Ok(Some(s)) => Ok(s.as_str().to_string()),
142+
Ok(None) => Err(MalformedResponseError {
143+
reason: "missing required `message` property within error".to_owned(),
144+
}),
145+
Err(err) => Err(MalformedResponseError {
145146
reason: format!("invalid `message` within error: {}", err),
146-
})?
147-
.map(|s| s.as_str().to_string())
148-
.unwrap_or_default();
147+
}),
148+
}?;
149149
let locations = extract_key_value_from_object!(object, "locations")
150150
.map(skip_invalid_locations)
151151
.map(serde_json_bytes::from_value)
152152
.transpose()
153-
.map_err(|err| FetchError::SubrequestMalformedResponse {
154-
service: service_name.to_string(),
153+
.map_err(|err| MalformedResponseError {
155154
reason: format!("invalid `locations` within error: {}", err),
156155
})?
157156
.unwrap_or_default();
158157
let path = extract_key_value_from_object!(object, "path")
159158
.map(serde_json_bytes::from_value)
160159
.transpose()
161-
.map_err(|err| FetchError::SubrequestMalformedResponse {
162-
service: service_name.to_string(),
160+
.map_err(|err| MalformedResponseError {
163161
reason: format!("invalid `path` within error: {}", err),
164162
})?;
165163

apollo-router/src/graphql/request.rs

Lines changed: 38 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ use serde::de::DeserializeSeed;
66
use serde::de::Error;
77
use serde_json_bytes::ByteString;
88
use serde_json_bytes::Map as JsonMap;
9-
use serde_json_bytes::Value;
109

1110
use crate::configuration::BatchingMode;
1211
use crate::json_ext::Object;
12+
use crate::json_ext::Value;
1313

1414
/// A GraphQL `Request` used to represent both supergraph and subgraph requests.
1515
#[derive(Clone, Derivative, Serialize, Deserialize, Default)]
@@ -169,34 +169,33 @@ impl Request {
169169
pub(crate) fn batch_from_urlencoded_query(
170170
url_encoded_query: String,
171171
) -> Result<Vec<Request>, serde_json::Error> {
172-
let value: serde_json::Value = serde_urlencoded::from_bytes(url_encoded_query.as_bytes())
172+
let value: Value = serde_urlencoded::from_bytes(url_encoded_query.as_bytes())
173173
.map_err(serde_json::Error::custom)?;
174174

175-
Request::process_query_values(&value)
175+
Request::process_query_values(value)
176176
}
177177

178178
/// Convert Bytes into a GraphQL [`Request`].
179179
///
180180
/// An error will be produced in the event that the bytes array cannot be
181181
/// turned into a valid GraphQL `Request`.
182182
pub(crate) fn batch_from_bytes(bytes: &[u8]) -> Result<Vec<Request>, serde_json::Error> {
183-
let value: serde_json::Value =
184-
serde_json::from_slice(bytes).map_err(serde_json::Error::custom)?;
183+
let value: Value = serde_json::from_slice(bytes).map_err(serde_json::Error::custom)?;
185184

186-
Request::process_batch_values(&value)
185+
Request::process_batch_values(value)
187186
}
188187

189-
fn allocate_result_array(value: &serde_json::Value) -> Vec<Request> {
188+
fn allocate_result_array(value: &Value) -> Vec<Request> {
190189
match value.as_array() {
191190
Some(array) => Vec::with_capacity(array.len()),
192191
None => Vec::with_capacity(1),
193192
}
194193
}
195194

196-
fn process_batch_values(value: &serde_json::Value) -> Result<Vec<Request>, serde_json::Error> {
197-
let mut result = Request::allocate_result_array(value);
195+
fn process_batch_values(value: Value) -> Result<Vec<Request>, serde_json::Error> {
196+
let mut result = Request::allocate_result_array(&value);
198197

199-
if let serde_json::Value::Array(entries) = value {
198+
if let Value::Array(entries) = value {
200199
u64_histogram!(
201200
"apollo.router.operations.batching.size",
202201
"Number of queries contained within each query batch",
@@ -211,20 +210,20 @@ impl Request {
211210
mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now
212211
);
213212
for entry in entries {
214-
let bytes = serde_json::to_vec(entry)?;
213+
let bytes = serde_json::to_vec(&entry)?;
215214
result.push(Request::deserialize_from_bytes(&bytes.into())?);
216215
}
217216
} else {
218-
let bytes = serde_json::to_vec(value)?;
217+
let bytes = serde_json::to_vec(&value)?;
219218
result.push(Request::deserialize_from_bytes(&bytes.into())?);
220219
}
221220
Ok(result)
222221
}
223222

224-
fn process_query_values(value: &serde_json::Value) -> Result<Vec<Request>, serde_json::Error> {
225-
let mut result = Request::allocate_result_array(value);
223+
fn process_query_values(value: Value) -> Result<Vec<Request>, serde_json::Error> {
224+
let mut result = Request::allocate_result_array(&value);
226225

227-
if let serde_json::Value::Array(entries) = value {
226+
if let Value::Array(entries) = value {
228227
u64_histogram!(
229228
"apollo.router.operations.batching.size",
230229
"Number of queries contained within each query batch",
@@ -239,42 +238,36 @@ impl Request {
239238
mode = BatchingMode::BatchHttpLink.to_string() // Only supported mode right now
240239
);
241240
for entry in entries {
242-
result.push(Request::process_value(entry)?);
241+
result.push(Request::process_value(&entry)?);
243242
}
244243
} else {
245-
result.push(Request::process_value(value)?)
244+
result.push(Request::process_value(&value)?)
246245
}
247246
Ok(result)
248247
}
249248

250-
fn process_value(value: &serde_json::Value) -> Result<Request, serde_json::Error> {
251-
let operation_name =
252-
if let Some(serde_json::Value::String(operation_name)) = value.get("operationName") {
253-
Some(operation_name.clone())
254-
} else {
255-
None
256-
};
257-
258-
let query = if let Some(serde_json::Value::String(query)) = value.get("query") {
259-
Some(query.as_str())
260-
} else {
261-
None
262-
};
263-
let variables: Object = get_from_urlencoded_value(value, "variables")?.unwrap_or_default();
264-
let extensions: Object =
265-
get_from_urlencoded_value(value, "extensions")?.unwrap_or_default();
266-
267-
let request_builder = Self::builder()
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)
268267
.variables(variables)
269268
.and_operation_name(operation_name)
270-
.extensions(extensions);
271-
272-
let request = if let Some(query_str) = query {
273-
request_builder.query(query_str).build()
274-
} else {
275-
request_builder.build()
276-
};
277-
269+
.extensions(extensions)
270+
.build();
278271
Ok(request)
279272
}
280273

@@ -284,25 +277,13 @@ impl Request {
284277
/// An error will be produced in the event that the query string parameters
285278
/// cannot be turned into a valid GraphQL `Request`.
286279
pub fn from_urlencoded_query(url_encoded_query: String) -> Result<Request, serde_json::Error> {
287-
let urldecoded: serde_json::Value =
288-
serde_urlencoded::from_bytes(url_encoded_query.as_bytes())
289-
.map_err(serde_json::Error::custom)?;
280+
let urldecoded: Value = serde_urlencoded::from_bytes(url_encoded_query.as_bytes())
281+
.map_err(serde_json::Error::custom)?;
290282

291283
Request::process_value(&urldecoded)
292284
}
293285
}
294286

295-
fn get_from_urlencoded_value<'a, T: Deserialize<'a>>(
296-
object: &'a serde_json::Value,
297-
key: &str,
298-
) -> Result<Option<T>, serde_json::Error> {
299-
if let Some(serde_json::Value::String(byte_string)) = object.get(key) {
300-
Some(serde_json::from_str(byte_string.as_str())).transpose()
301-
} else {
302-
Ok(None)
303-
}
304-
}
305-
306287
struct RequestFromBytesSeed<'data>(&'data Bytes);
307288

308289
impl<'de> DeserializeSeed<'de> for RequestFromBytesSeed<'_> {

0 commit comments

Comments
 (0)