Skip to content

feat(execution): expose SubgraphHeadersBuilder and ExecutionOptions as execution options - #1617

Open
Daniil-K wants to merge 4 commits into
wundergraph:masterfrom
Daniil-K:feat/subgraph-headers-builder-execution-option
Open

Daniil-K wants to merge 4 commits into
wundergraph:masterfrom
Daniil-K:feat/subgraph-headers-builder-execution-option

Conversation

@Daniil-K

@Daniil-K Daniil-K commented Aug 4, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added configuration options for customizing subgraph request headers and execution behavior.
    • Added support for distinct per-request headers while safely deduplicating equivalent concurrent subgraph requests.
    • Added an option to disable subgraph request deduplication when each execution must be fetched independently.
  • Documentation

    • Clarified deduplication behavior, including how requests are matched and how per-request headers should be provided.

Fixes #1616.

What

Adds two execution options to execution/engine:

// Sets the builder that produces the headers for subgraph requests, and whose
// hash the resolver folds into its request deduplication keys.
func WithSubgraphHeadersBuilder(builder resolve.SubgraphHeadersBuilder) ExecutionOptions

// Sets resolve.ExecutionOptions, e.g. to turn request deduplication off.
func WithExecutionOptions(options resolve.ExecutionOptions) ExecutionOptions

and corrects the documented scope of
resolve.ExecutionOptions.DisableSubgraphRequestDeduplication.

Why

resolve.Context already carries SubgraphHeadersBuilder and ExecutionOptions,
but ExecutionEngine builds its resolve.Context internally and
type ExecutionOptions func(ctx *internalExecutionContext) takes an unexported
struct, so no function of that type can be declared outside the package. Both
fields are therefore unreachable for embedders.

That is a correctness gap rather than a missing convenience. The subgraph
deduplication key is
hash(DataSourceID + renderedInput + SubgraphHeadersBuilder hash). With no
builder the hash is 0, so per-client headers forwarded by any other route — an
http.RoundTripper on the data source client is the only option
execution/engine leaves — are not part of the key. Concurrent operations with
byte-identical subgraph request bodies collapse into one fetch, and the followers
copy the leader's response without ever issuing a request of their own. Every
argument-free operation has an identical body across clients, so concurrent
clients receive each other's data.

The issue has a self-contained reproducer. On an idle machine, eight concurrent
clients produce one upstream fetch and seven responses carrying another client's
data.

With WithSubgraphHeadersBuilder the headers travel through the engine, become
part of the key, and deduplication keeps working for callers that genuinely do
share headers — which is why this is preferable to just switching the feature
off. WithExecutionOptions is there for embedders who would rather disable
deduplication outright.

Tests

New file execution/engine/execution_engine_deduplication_test.go:

  • TestWithSubgraphHeadersBuilder/sets the builder on the resolve context
  • TestWithSubgraphHeadersBuilder/concurrent identical operations resolve against their own headers
    8 concurrent executions, 8 distinct header sets, each response matches its own
    headers and the subgraph is hit exactly 8 times
  • TestWithSubgraphHeadersBuilder/identical headers are still deduplicated
    8 concurrent executions sharing one header set collapse into fewer fetches, so
    the option does not disable single flight
  • TestWithExecutionOptions/sets the execution options on the resolve context
  • TestWithExecutionOptions/disabling subgraph deduplication gives every execution its own fetch

The subgraph in these tests echoes the Authorization header it received, so a
response identifies which caller's headers the fetch was made with, and a fixed
delay keeps the single flight window open for the whole burst.

SubgraphHeadersBuilder had no test coverage before this change.

Verified locally: go test ./engine/ passes, and
go test -race ./engine/ -run 'TestWithSubgraphHeadersBuilder|TestWithExecutionOptions' -count=3
passes with no races.

Compatibility

Additive. Nothing changes for callers that do not pass the new options; both
default to today's behaviour.

Note for implementers

A SubgraphHeadersBuilder must return a copy of its header map from
HeadersForSubgraph. httpclient.makeHTTPRequest assigns the returned value
straight into http.Request.Header and then adds Accept and Content-Type to
it, so a shared map races across concurrent fetches. This is documented on the
new option.

Checklist

  • I have discussed my proposed changes in an issue and have received approval to proceed.
  • I have followed the coding standards of the project.
  • Tests or benchmarks have been added or updated.

The first box is intentionally unchecked: I opened #1616 at the same time as this
PR rather than waiting for approval first, because the change is small and easier
to judge with the code in front of you. Happy to close this and continue in the
issue if you would prefer to settle the approach there.

Open Source AI Manifesto

I have read and verified every line of this change, and the reproducer and tests
back it up.

resolve.Context exposes SubgraphHeadersBuilder and ExecutionOptions, but
ExecutionEngine builds its resolve.Context internally and the functional
option type takes an unexported struct, so neither field can be reached
from outside the engine package.

That matters for correctness, not just convenience. The resolver folds the
hash returned by SubgraphHeadersBuilder into its subgraph request
deduplication key. Without a builder the hash is zero, so the key covers
only the data source ID and the rendered request body. A caller that
forwards per-client headers to subgraphs by another route - typically an
http.RoundTripper installed on the data source HTTP client - contributes
nothing to the key. Concurrent operations whose subgraph request bodies
are byte-identical, which is every operation without arguments, then
collapse into one fetch and all callers receive the response resolved for
one arbitrary client.

Add two options so the fields become reachable:

  - WithSubgraphHeadersBuilder makes header forwarding go through the
    engine, which puts it in the deduplication key while keeping
    deduplication effective for callers that do share headers.
  - WithExecutionOptions sets resolve.ExecutionOptions, which lets callers
    turn deduplication off outright.

Both follow the shape of the existing WithAuthorizer and
WithPreFetchFieldAuthorizer options. No behaviour changes unless an option
is passed.
DisableSubgraphRequestDeduplication was documented as covering requests
"within a single operation execution". A Resolver holds one
SubgraphRequestSingleFlight for its whole lifetime and hands it to every
resolve call, so deduplication spans concurrent operations from different
clients.

The distinction is easy to get wrong and the consequence is a cross-client
data leak, so spell out the real scope, what the key covers, and that
per-client headers have to reach subgraphs through SubgraphHeadersBuilder
to be part of it.
@Daniil-K
Daniil-K requested a review from a team as a code owner August 4, 2026 10:14
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 241f1277-7b38-404a-b479-ab7d44cd480b

📥 Commits

Reviewing files that changed from the base of the PR and between 88edf3d and 5e82d39.

📒 Files selected for processing (1)
  • execution/engine/execution_engine_deduplication_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • execution/engine/execution_engine_deduplication_test.go

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The execution engine now accepts subgraph header builders and resolver execution options. Tests cover concurrent header propagation, request deduplication, and disabled deduplication. Resolver documentation defines the deduplication scope and key.

Changes

Execution options and request deduplication

Layer / File(s) Summary
Expose execution options
execution/engine/execution_engine.go, v2/pkg/engine/resolve/context.go
Adds WithSubgraphHeadersBuilder and WithExecutionOptions. Documents resolver-lifetime deduplication and its key components.
Build concurrent subgraph test flow
execution/engine/execution_engine_deduplication_test.go
Adds an HTTP subgraph fixture, engine setup, header generation, and synchronized concurrent execution helpers.
Validate header identity and deduplication
execution/engine/execution_engine_deduplication_test.go
Tests distinct and identical header behavior, option storage, and disabled subgraph request deduplication.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 5e82d

This additive change exposes execution options for subgraph headers and request deduplication without introducing an actionable merge-blocking risk; it is merge-ready after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ExecutionEngine
  participant ResolveContext
  participant Resolver
  participant Subgraph
  Client->>ExecutionEngine: Execute operation with execution options
  ExecutionEngine->>ResolveContext: configure headers and resolver options
  ResolveContext->>Resolver: resolve operation
  Resolver->>Subgraph: send rendered request with generated headers
  Subgraph-->>Resolver: return response
  Resolver-->>ExecutionEngine: return GraphQL result
  ExecutionEngine-->>Client: return operation result
Loading
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: exposing SubgraphHeadersBuilder and ExecutionOptions through execution options.
Linked Issues check ✅ Passed The changes satisfy issue #1616 by exposing both required options, supporting per-client subgraph header hashes in deduplication keys, allowing deduplication to be disabled, correcting the deduplicati…
Out of Scope Changes check ✅ Passed The changes remain within issue #1616 scope. The API additions, documentation updates, deduplication tests, and WaitGroup.Go modernization directly support or validate the requested behavior.
Full details: Linked Issues check

Explanation

The changes satisfy issue #1616 by exposing both required options, supporting per-client subgraph header hashes in deduplication keys, allowing deduplication to be disabled, correcting the deduplication scope documentation, and adding focused tests.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@execution/engine/execution_engine_deduplication_test.go`:
- Around line 141-168: Update the concurrent worker setup around the goroutine
in the deduplication test to add a per-worker readiness signal after creating
its request and result writer. Wait for all workers to report readiness before
closing start, ensuring every worker is blocked on the start receive before
execution begins; preserve the existing WaitGroup and result assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 40c8dbf9-dfe5-44d0-b22e-7b090b7b8b24

📥 Commits

Reviewing files that changed from the base of the PR and between 165b1f5 and bc30be1.

📒 Files selected for processing (3)
  • execution/engine/execution_engine.go
  • execution/engine/execution_engine_deduplication_test.go
  • v2/pkg/engine/resolve/context.go

Comment thread execution/engine/execution_engine_deduplication_test.go
executeConcurrently closed the start channel as soon as the loop that spawns
the workers finished, so a worker that had not reached the receive yet would
begin late. With a 50ms subgraph delay the margin is wide, but on a loaded
runner a straggler could enter Execute after the leader's fetch had already
completed, and the shared-header case would then be measuring scheduling
rather than deduplication.

Have each worker report once its setup is done and release the burst only
after every report has arrived.
@SoulPancake

Copy link
Copy Markdown
Contributor

woo strange emoji in your PR title

@Daniil-K Daniil-K changed the title ✨feat(execution): expose SubgraphHeadersBuilder and ExecutionOptions as execution options feat(execution): expose SubgraphHeadersBuilder and ExecutionOptions as execution options Aug 11, 2026
@Daniil-K

Daniil-K commented Aug 11, 2026

Copy link
Copy Markdown
Author

Fair — dropped it. I took it from the examples in the PR template (✨feat(auth): ...), but looking at the merged PRs nobody actually uses emoji, so the template is the odd one out here. Title is now plain feat(execution): ....

While you are here: is there anything needed from my side to get CI running? No checks have been triggered on the branch, which I assume is the usual approval gate for a first-time contributor. Happy to rebase onto master too — the branch is a few days behind now.

@SoulPancake

SoulPancake commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Sorry I am not a maintainer
@devsergiy @pepol might be able to help

@Daniil-K

Copy link
Copy Markdown
Author

@devsergiy @pepol Help please

golangci-lint's modernize analyzer flags the Add(1) / defer Done() pair now
that Go 1.25 provides WaitGroup.Go, which does both itself.
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.

Subgraph request deduplication ignores per-client headers forwarded outside the engine

2 participants