Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prioritize errors in response #4728

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changesets/fix_nc_prioritize_errors_in_response.md
@@ -0,0 +1,7 @@
### Prioritize errors in GraphQL response ([Issue #4375](https://github.com/apollographql/router/issues/4375))

Previously, the router would return data from an operation before any potential errors in the request.
[As recommended in the GraphQL spec](https://spec.graphql.org/draft/#note-6f005), the suggested route
is to try to return errors first before data in the response. The router now does so.

By [@nicholascioli](https://github.com/nicholascioli) in https://github.com/apollographql/router/pull/4728
Expand Up @@ -4,6 +4,14 @@ expression: parts
---
[
{
"errors": [
{
"message": "couldn't find mock for query {\"query\":\"query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{__typename id product{__typename upc}}}}}\",\"operationName\":\"TopProducts__reviews__1\",\"variables\":{\"representations\":[{\"__typename\":\"Product\",\"upc\":\"1\"},{\"__typename\":\"Product\",\"upc\":\"2\"}]}}",
"extensions": {
"code": "FETCH_ERROR"
}
}
],
"data": {
"topProducts": [
{
Expand All @@ -18,14 +26,6 @@ expression: parts
}
]
},
"errors": [
{
"message": "couldn't find mock for query {\"query\":\"query TopProducts__reviews__1($representations:[_Any!]!){_entities(representations:$representations){...on Product{reviews{__typename id product{__typename upc}}}}}\",\"operationName\":\"TopProducts__reviews__1\",\"variables\":{\"representations\":[{\"__typename\":\"Product\",\"upc\":\"1\"},{\"__typename\":\"Product\",\"upc\":\"2\"}]}}",
"extensions": {
"code": "FETCH_ERROR"
}
}
],
"hasNext": true
},
{
Expand Down
Expand Up @@ -3,16 +3,6 @@ source: apollo-router/src/plugins/authorization/authenticated.rs
expression: response
---
{
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": null
}
}
},
"errors": [
{
"message": "Unauthorized field or type",
Expand All @@ -25,5 +15,15 @@ expression: response
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
],
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": null
}
}
}
}
Expand Up @@ -3,16 +3,6 @@ source: apollo-router/src/plugins/authorization/tests.rs
expression: response
---
{
"data": {
"orga": {
"id": null,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": null
}
}
},
"errors": [
{
"message": "Unauthorized field or type",
Expand All @@ -35,5 +25,15 @@ expression: response
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
],
"data": {
"orga": {
"id": null,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": null
}
}
}
}
Expand Up @@ -3,16 +3,6 @@ source: apollo-router/src/plugins/authorization/tests.rs
expression: response
---
{
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": "1234"
}
}
},
"errors": [
{
"message": "Unauthorized field or type",
Expand All @@ -35,5 +25,15 @@ expression: response
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
],
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": "1234"
}
}
}
}
Expand Up @@ -3,7 +3,6 @@ source: apollo-router/src/plugins/authorization/tests.rs
expression: response
---
{
"data": {},
"errors": [
{
"message": "Unauthorized field or type",
Expand All @@ -26,5 +25,6 @@ expression: response
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
],
"data": {}
}
Expand Up @@ -3,16 +3,6 @@ source: apollo-router/src/plugins/authorization/tests.rs
expression: response
---
{
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": null
}
}
},
"errors": [
{
"message": "Unauthorized field or type",
Expand All @@ -25,5 +15,15 @@ expression: response
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
],
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": null
}
}
}
}
Expand Up @@ -3,16 +3,6 @@ source: apollo-router/src/plugins/authorization/tests.rs
expression: response
---
{
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": null
}
}
},
"errors": [
{
"message": "Unauthorized field or type",
Expand All @@ -25,5 +15,15 @@ expression: response
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
],
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": null
}
}
}
}
Expand Up @@ -3,12 +3,6 @@ source: apollo-router/src/plugins/authorization/tests.rs
expression: response
---
{
"data": {
"orga": {
"id": 1,
"creatorUser": null
}
},
"errors": [
{
"message": "Unauthorized field or type",
Expand All @@ -20,5 +14,11 @@ expression: response
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
],
"data": {
"orga": {
"id": 1,
"creatorUser": null
}
}
}
Expand Up @@ -3,16 +3,6 @@ source: apollo-router/src/plugins/authorization/tests.rs
expression: response
---
{
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": "1234"
}
}
},
"errors": [
{
"message": "Unauthorized field or type",
Expand All @@ -35,5 +25,15 @@ expression: response
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
],
"data": {
"orga": {
"id": 1,
"creatorUser": {
"id": 0,
"name": "Ada",
"phone": "1234"
}
}
}
}
Expand Up @@ -3,7 +3,6 @@ source: apollo-router/src/plugins/authorization/tests.rs
expression: response
---
{
"data": {},
"errors": [
{
"message": "Unauthorized field or type",
Expand All @@ -15,5 +14,6 @@ expression: response
"code": "UNAUTHORIZED_FIELD_OR_TYPE"
}
}
]
],
"data": {}
}
6 changes: 3 additions & 3 deletions apollo-router/src/plugins/include_subgraph_errors.rs
Expand Up @@ -102,19 +102,19 @@ mod test {
use crate::Configuration;

static UNREDACTED_PRODUCT_RESPONSE: Lazy<Bytes> = Lazy::new(|| {
Bytes::from_static(r#"{"data":{"topProducts":null},"errors":[{"message":"couldn't find mock for query {\"query\":\"query ErrorTopProducts__products__0($first:Int){topProducts(first:$first){__typename upc name}}\",\"operationName\":\"ErrorTopProducts__products__0\",\"variables\":{\"first\":2}}","extensions":{"test":"value","code":"FETCH_ERROR"}}]}"#.as_bytes())
Bytes::from_static(r#"{"errors":[{"message":"couldn't find mock for query {\"query\":\"query ErrorTopProducts__products__0($first:Int){topProducts(first:$first){__typename upc name}}\",\"operationName\":\"ErrorTopProducts__products__0\",\"variables\":{\"first\":2}}","extensions":{"test":"value","code":"FETCH_ERROR"}}],"data":{"topProducts":null}}"#.as_bytes())
});

static REDACTED_PRODUCT_RESPONSE: Lazy<Bytes> = Lazy::new(|| {
Bytes::from_static(
r#"{"data":{"topProducts":null},"errors":[{"message":"Subgraph errors redacted"}]}"#
r#"{"errors":[{"message":"Subgraph errors redacted"}],"data":{"topProducts":null}}"#
.as_bytes(),
)
});

static REDACTED_ACCOUNT_RESPONSE: Lazy<Bytes> = Lazy::new(|| {
Bytes::from_static(
r#"{"data":null,"errors":[{"message":"Subgraph errors redacted"}]}"#.as_bytes(),
r#"{"errors":[{"message":"Subgraph errors redacted"}],"data":null}"#.as_bytes(),
)
});

Expand Down
Expand Up @@ -42,7 +42,6 @@ expression: yaml
name: supergraph
otel.kind: INTERNAL
- fields:
http.response.body: "Response { label: None, data: Some(Object({\"data\": String(\"res\")})), path: None, errors: [], extensions: {}, has_next: None, subscribed: None, created_at: None, incremental: [] }"
http.response.body: "Response { label: None, errors: [], data: Some(Object({\"data\": String(\"res\")})), path: None, extensions: {}, has_next: None, subscribed: None, created_at: None, incremental: [] }"
level: INFO
message: Supergraph GraphQL response

30 changes: 26 additions & 4 deletions apollo-router/src/response.rs
Expand Up @@ -23,6 +23,10 @@ pub struct Response {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub label: Option<String>,

/// The optional graphql errors encountered.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub errors: Vec<Error>,

/// The response data.
#[serde(skip_serializing_if = "Option::is_none", default)]
pub data: Option<Value>,
Expand All @@ -31,10 +35,6 @@ pub struct Response {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub path: Option<Path>,

/// The optional graphql errors encountered.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub errors: Vec<Error>,

/// The optional graphql extensions.
#[serde(skip_serializing_if = "Object::is_empty", default)]
pub extensions: Object,
Expand Down Expand Up @@ -466,4 +466,26 @@ mod tests {
}
);
}

#[test]
fn test_errors_come_first() {
let response = Response::builder()
.error(
Error::builder()
.message("some random error")
.extension_code("RANDOM_ERROR")
.build(),
)
.data(json!({
"random": "data"
}))
.build();

let json = serde_json::to_string(&response).unwrap();
let errors_location = json.find(r#""errors":"#).unwrap();
let data_location = json.find(r#""data":"#).unwrap();

// Make sure that errors are present before data
assert!(errors_location < data_location)
}
}
Expand Up @@ -5,6 +5,7 @@ expression: "(graphql_response, &subgraph_query_log)"
(
Response {
label: None,
errors: [],
data: Some(
Object({
"topProducts": Array([
Expand All @@ -24,7 +25,6 @@ expression: "(graphql_response, &subgraph_query_log)"
}),
),
path: None,
errors: [],
extensions: {},
has_next: None,
subscribed: None,
Expand Down