-
Notifications
You must be signed in to change notification settings - Fork 330
Reliably distinguish GraphQL errors and transport errors in subscriptions #7901
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| ### Reliably distinguish GraphQL errors and transport errors in subscriptions ([PR #7901](https://github.com/apollographql/router/pull/7901)) | ||
|
|
||
| The [Multipart HTTP protocol for GraphQL Subscriptions](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol) distinguishes between GraphQL-level errors and fatal transport-level errors. The router previously used a heuristic to determine if a given error was fatal or not, which could sometimes cause errors to be wrongly classified. For example, if a subgraph returned a GraphQL-level error for a subscription and then immediately ended the subscription, the router might propagate this as a fatal transport-level error. | ||
|
|
||
| This is now fixed. Fatal transport-level errors are tagged as such when they are constructed, so the router can reliably know how to serialize errors when sending them to the client. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ use tokio_stream::once; | |
| use tokio_stream::wrappers::IntervalStream; | ||
|
|
||
| use crate::graphql; | ||
| use crate::services::SUBSCRIPTION_ERROR_EXTENSION_KEY; | ||
|
|
||
| #[cfg(test)] | ||
| const HEARTBEAT_INTERVAL: Duration = Duration::from_millis(10); | ||
|
|
@@ -115,27 +116,39 @@ impl Stream for Multipart { | |
|
|
||
| match self.mode { | ||
| ProtocolMode::Subscription => { | ||
| let resp = SubscriptionPayload { | ||
| errors: if is_still_open { | ||
| Vec::new() | ||
| } else { | ||
| response.errors.drain(..).collect() | ||
| }, | ||
| payload: match response.data { | ||
| None | Some(Value::Null) if response.extensions.is_empty() => { | ||
| None | ||
| } | ||
| _ => (*response).into(), | ||
| }, | ||
| }; | ||
|
|
||
| // Gracefully closed at the server side | ||
| if !is_still_open && resp.payload.is_none() && resp.errors.is_empty() { | ||
| let is_transport_error = | ||
| response.extensions.remove(SUBSCRIPTION_ERROR_EXTENSION_KEY) | ||
| == Some(true.into()); | ||
| // Magic empty response (that we create internally) means the connection was gracefully closed at the server side | ||
| if !is_still_open | ||
| && response.data.is_none() | ||
| && response.errors.is_empty() | ||
| && response.extensions.is_empty() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the significance of extensions being empty? If something other than the subscriptions error key was in extensions then will that break graceful close?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess if extensions is not empty then we want to return the response
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Essentially I'm trying to mimick what the previous code does. In the previous code, I'm not sure if this is like actually required, to be honest. The subscriptions graceful close response can only have extensions if a plugin or user code added them. |
||
| { | ||
| self.is_terminated = true; | ||
| return Poll::Ready(Some(Ok(Bytes::from_static(&b"--\r\n"[..])))); | ||
| } else { | ||
| serde_json::to_writer(&mut buf, &resp)?; | ||
| } | ||
|
|
||
| let response = if is_transport_error { | ||
| SubscriptionPayload { | ||
| errors: std::mem::take(&mut response.errors), | ||
| payload: match response.data { | ||
| None | Some(Value::Null) | ||
| if response.extensions.is_empty() => | ||
| { | ||
| None | ||
| } | ||
| _ => (*response).into(), | ||
| }, | ||
| } | ||
| } else { | ||
| SubscriptionPayload { | ||
| errors: Vec::new(), | ||
| payload: (*response).into(), | ||
| } | ||
| }; | ||
|
|
||
| serde_json::to_writer(&mut buf, &response)?; | ||
| } | ||
| ProtocolMode::Defer => { | ||
| serde_json::to_writer(&mut buf, &response)?; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where the actual change happens.
Now, we decide whether to send
payloadorerrorsin the response exclusively based on theSUBSCRIPTION_ERROR_EXTENSION_KEYbeingtrue.The code is reordered a bit (special handling for graceful server-side close is moved earlier) so we aren't creating another special-meaning internal message (
SubscriptionPayloadwithpayloadbeing an empty object/null...)