Skip to content

enhance(executor): refactor subgraph executor return type#651

Merged
kamilkisiela merged 6 commits intomainfrom
subgraph_response
Jan 14, 2026
Merged

enhance(executor): refactor subgraph executor return type#651
kamilkisiela merged 6 commits intomainfrom
subgraph_response

Conversation

@ardatan
Copy link
Member

@ardatan ardatan commented Jan 12, 2026

Extracted from #628

  • SubgraphExecutor now returns SubgraphResponse that has the deserialized response in it with bytes: Bytes to be stored in the response storage later on.
  • HTTPSubgraphExecutor is now responsible of deserialization instead of Plan executor.
  • No need for Arc<Bytes> because Bytes already uses Arc internally so it has 0 cost of cloning

Why?
In the plugin system's on_subgraph_execute hook is expected to return SubgraphResponse which is basically data, errors and extensions (of course together with an optional bytes and headers).
In order to have a better DX, I wanted to make this flow's return value SubgraphResponse while keeping HttpResponse as on_subgraph_http_request's return value.
In this way SubgraphResponse becomes HTTP-independent, and it will be easy to handle errors and etc without generating bytes and doing deserialization and serialization work etc for prebuilt responses.
For example graphql_error_to_bytes is no longer needed with this. It can be just SubgraphResponse { errors: vec![error], ...Default:default()

@ardatan ardatan requested review from dotansimha and kamilkisiela and removed request for dotansimha January 12, 2026 11:44
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ardatan, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the subgraph execution process by centralizing deserialization within the HTTPSubgraphExecutor and standardizing the return type of SubgraphExecutor to SubgraphResponse. This change simplifies the overall data handling, improves efficiency by leveraging the Bytes type's internal Arc for cloning, and enhances error reporting for deserialization failures. The modifications aim to create a more robust and performant execution pipeline.

Highlights

  • Refactored SubgraphExecutor Return Type: The SubgraphExecutor trait now directly returns a SubgraphResponse object, which includes the deserialized response data, headers, and raw bytes, streamlining the data flow.
  • Deserialization Responsibility Shift: The HTTPSubgraphExecutor is now responsible for deserializing the HTTP response into a SubgraphResponse, moving this logic out of the PlanExecutor.
  • Optimized Byte Handling: The use of Arc<Bytes> has been removed, as the Bytes type itself internally uses Arc for efficient, zero-cost cloning, reducing overhead.
  • New Error Type for Deserialization: A new error variant, ResponseDeserializationFailure, has been introduced in SubgraphExecutorError to specifically handle issues during subgraph response deserialization.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively refactors the SubgraphExecutor to return a SubgraphResponse containing the deserialized data, which simplifies the execution plan logic. Moving deserialization into the HTTPSubgraphExecutor is a good separation of concerns. The removal of the redundant Arc<Bytes> is also a nice cleanup. I've found one critical issue where subgraph errors could be silently dropped, and a high-severity issue with an incorrect safety comment for an unsafe block that should be addressed.

@github-actions
Copy link

github-actions bot commented Jan 12, 2026

k6-benchmark results

     ✓ response code was 200
     ✓ no graphql errors
     ✓ valid response structure

     █ setup

     checks.........................: 100.00% ✓ 235029      ✗ 0    
     data_received..................: 6.9 GB  228 MB/s
     data_sent......................: 92 MB   3.1 MB/s
     http_req_blocked...............: avg=3.5µs    min=641ns   med=1.78µs  max=4.02ms   p(90)=2.52µs  p(95)=2.86µs  
     http_req_connecting............: avg=610ns    min=0s      med=0s      max=2.26ms   p(90)=0s      p(95)=0s      
     http_req_duration..............: avg=18.67ms  min=1.91ms  med=17.73ms max=78.02ms  p(90)=25.84ms p(95)=28.92ms 
       { expected_response:true }...: avg=18.67ms  min=1.91ms  med=17.73ms max=78.02ms  p(90)=25.84ms p(95)=28.92ms 
     http_req_failed................: 0.00%   ✓ 0           ✗ 78363
     http_req_receiving.............: avg=134.16µs min=23.55µs med=38.79µs max=40.21ms  p(90)=86.53µs p(95)=379.37µs
     http_req_sending...............: avg=23.46µs  min=5.44µs  med=10.52µs max=23.95ms  p(90)=15.55µs p(95)=26.65µs 
     http_req_tls_handshaking.......: avg=0s       min=0s      med=0s      max=0s       p(90)=0s      p(95)=0s      
     http_req_waiting...............: avg=18.51ms  min=1.87ms  med=17.6ms  max=77.15ms  p(90)=25.58ms p(95)=28.64ms 
     http_reqs......................: 78363   2606.627225/s
     iteration_duration.............: avg=19.13ms  min=5ms     med=18.08ms max=207.98ms p(90)=26.31ms p(95)=29.41ms 
     iterations.....................: 78343   2605.961955/s
     vus............................: 50      min=50        max=50 
     vus_max........................: 50      min=50        max=50 

@github-actions
Copy link

github-actions bot commented Jan 12, 2026

🐋 This PR was built and pushed to the following Docker images:

Image Names: ghcr.io/graphql-hive/router

Platforms: linux/amd64,linux/arm64

Image Tags: ghcr.io/graphql-hive/router:pr-651 ghcr.io/graphql-hive/router:sha-a227493

Docker metadata
{
"buildx.build.ref": "builder-44795178-363d-4c3b-a49e-703d557730c3/builder-44795178-363d-4c3b-a49e-703d557730c30/jfxyuloaedg5l9p90uptlugh5",
"containerimage.descriptor": {
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "digest": "sha256:39ac5b07e658f652e5bf0d70d0842fba4472bb16dfc65d31574201e6adabbffb",
  "size": 1609
},
"containerimage.digest": "sha256:39ac5b07e658f652e5bf0d70d0842fba4472bb16dfc65d31574201e6adabbffb",
"image.name": "ghcr.io/graphql-hive/router:pr-651,ghcr.io/graphql-hive/router:sha-a227493"
}

@ardatan ardatan removed the request for review from kamilkisiela January 12, 2026 12:14
@ardatan ardatan enabled auto-merge (squash) January 12, 2026 12:57
ardatan and others added 4 commits January 12, 2026 11:44
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
// underlying data remains alive as long as the `SubgraphResponse` does.
// The `data` field of `SubgraphResponse` contains values that borrow from this buffer,
// creating a self-referential struct, which is why `unsafe` is required.
let bytes_ref: &'a [u8] = unsafe { std::mem::transmute(bytes_ref) };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it's a good idea and it won't kick us in the nuts at some point.

Previously, we passed Bytes to a function that both added the bytes to a response storage (so we knew how long the response is hold in memory) AND did this unsafe transmute trick (to match the storage response's lifetime).

It was safe unsafe, as we knew exactly the lifetime of both things and now we have a structure that enforces a certain lifetime on an object that is gone at the end of the function call, but also cloned.

I guess that's okay... as least we cover that in a description, and explain the behaviour there.

@kamilkisiela
Copy link
Contributor

@ardatan can you resolve the conflict and ping me?

@kamilkisiela kamilkisiela disabled auto-merge January 14, 2026 13:20
@ardatan ardatan requested a review from kamilkisiela January 14, 2026 13:26
@ardatan
Copy link
Member Author

ardatan commented Jan 14, 2026

@kamilkisiela Fixed the conflicts

@kamilkisiela kamilkisiela merged commit 006868c into main Jan 14, 2026
23 checks passed
@kamilkisiela kamilkisiela deleted the subgraph_response branch January 14, 2026 14:16
ardatan added a commit that referenced this pull request Jan 22, 2026
…cutor (#652)

Extracted from #628

Combining with #651 , the aim
is to reduce the number of extra structs.

Today, `PlanExecutionOutput` which is the return type of
`execute_query_plan` in `executor/execution/plan.rs`, returns `body:
Vec<u8>` and `headers: http::HeaderMap`.
Then, in the `router/pipeline`, `ResponseBuilder` is called with
`Bytes::from` of `PlanExecutionOutput.body` and
`PlanExecutionOutput.headers` is converted to `ntex::HeaderMap`, then
`content_type` is set here.

With this PR, the response aggregator is sent with
`PlanExecutionOutput`, and during the response build, the response
headers aggregator sets the headers into `ResponseBuilder` directly.

So now `modify_client_headers` helper accepts `ResponseBuilder`, not
`http:HeaderMap`. And since this throws `HeaderPropagationError` in the
pipeline executor, I had to add `HeaderPropagationError` to
`PipelineError`.

I also extracted `PipelineError` to `FailedExecutionResult` logic to a
`From` trait so that other future implementations that serializes and
sets the body in a different way can use it and serialize
`FailedExecutionResult` on their own.
Currently it is used here only ->
https://github.com/graphql-hive/router/pull/652/changes#diff-f3dc2f2d579374645281d6950414266d8984195dbf45c00f0a754ceca8550aadR95

----
Update:
After recent changes, I see that `project_by_operation` is used for
`dry_run` but it is actually unnecessary as we just need to serialize
`extensions` which has nothing to do with projection since it uses
`sonic_rs::Value`.
ardatan added a commit that referenced this pull request Feb 3, 2026
Extracted from #628

Now `SubgraphExecutor` can throw `SubgraphExecutorError` which will be
coerced to `PlanExecutorError`.
So that the execution related errors and the errors propagated from the
subgraph can be distinguished.
Combining this with #651, we
no longer need to generate dummy subgraph response objects with
`GraphQLError`s. It was a bit complicated before because in different
levels, helpers like `error_to_graphql_bytes` and
`internal_server_error_response` were handling those errors in different
places, so it was hard to debug it.

Right now the subgraph response will be the actual subgraph response
while the rest of the execution errors thrown by us will be handled as
`PlanExecutionError`.

Of course an error in a plan node will not stop the request entirely for
such cases like a subgraph is down etc which is tested in an E2E test
added with this PR.

To summarize the idea is to take the responsibility of `error` to
`response` conversion from the subgraph executor to the plan executor.

I also removed unused methods;
- HeaderRuleCompileError.new_expression_build
- HeaderRuleRuntimeError.new_value_conversion

And removed unnecessary methods;
- HeaderRuleRuntimeError.new_expression_evaluation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants