Skip to content

feat: allow rendering custom extensions in the final JSON response#1489

Open
Noroth wants to merge 5 commits intomasterfrom
ludwig/eng-9502-router-subgraph-extension-forwarding-support
Open

feat: allow rendering custom extensions in the final JSON response#1489
Noroth wants to merge 5 commits intomasterfrom
ludwig/eng-9502-router-subgraph-extension-forwarding-support

Conversation

@Noroth
Copy link
Copy Markdown
Contributor

@Noroth Noroth commented May 6, 2026

This PR adds options to add user defined extensions from subgraph responses to the final rendered JSON response. Currently only a small set of extensions are allowed to be returned. This PR allows users to configure root level extension fields that should be propagated to the client.

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.

Open Source AI Manifesto

This project follows the principles of the Open Source AI Manifesto. Please ensure your contribution aligns with its principles.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR captures top-level GraphQL response extensions from subgraphs (when enabled), stores them in Resolvable, filters/merges them by an allow-list and a forwarding algorithm (first_write/last_write), and renders allowed subgraph extensions alongside reserved extensions in responses.

Changes

Subgraph Extension Capture and Forwarding

Layer / File(s) Summary
Extension Types and Configuration
v2/pkg/engine/resolve/resolvable.go, v2/pkg/engine/resolve/resolve.go
ExtensionForwardingAlgorithm type with first_write/last_write; ResolvableOptions adds AllowedSubgraphExtensions and ExtensionForwardingAlgorithm; ResolverOptions adds AllowCustomExtensionProperties.
Resolvable Storage and Reset
v2/pkg/engine/resolve/resolvable.go
Resolvable adds subgraphExtensions slice and allowedExtensions map; Reset clears these fields per request.
Extension Filtering and Rendering
v2/pkg/engine/resolve/resolvable.go
filterAllowedSubgraphExtensions builds allowedExtensions applying allow-list and first_write/last_write merging, skips reserved keys via a default reserved set; hasExtensions and printExtensions include filtered subgraph extensions in output checks and rendering order.
Loader Extension Capture
v2/pkg/engine/resolve/loader.go
Loader adds allowCustomExtensionProperties; mergeResult appends parsed top-level extensions object into resolvable.subgraphExtensions when enabled and the extensions value is a non-null object.
Options Wiring
v2/pkg/engine/resolve/resolve.go
newTools propagates ResolverOptions.AllowCustomExtensionProperties into Resolvable/Loader configuration.
Test Coverage
v2/pkg/engine/resolve/loader_test.go, v2/pkg/engine/resolve/resolvable_test.go
Tests added for loader capture behavior, forwarding with/without allow-list, nested forwarding, duplicate-key resolution under both algorithms, algorithm validation/mapping, and reserved-extension preservation.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Loader
  participant Resolvable
  participant Printer
  Client->>Loader: execute fetch/merge
  Loader->>Resolvable: append parsed subgraph extensions
  Resolvable->>Resolvable: filterAllowedSubgraphExtensions (apply allow-list & algorithm)
  Resolvable->>Printer: printExtensions (include allowedExtensions)
  Printer-->>Client: final JSON response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: allowing custom extensions to be rendered in the final JSON response, which matches the primary change across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description accurately describes the changeset: adding options to allow user-defined extensions from subgraph responses to be propagated to the final rendered JSON response.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ludwig/eng-9502-router-subgraph-extension-forwarding-support

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

@Noroth Noroth marked this pull request as ready for review May 7, 2026 08:31
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@Noroth
Copy link
Copy Markdown
Contributor Author

Noroth commented May 7, 2026

@coderabbitai summary

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Summary regeneration triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
v2/pkg/engine/resolve/resolvable_test.go (1)

1433-1438: ⚡ Quick win

Use NewContext() in the helper.

This helper introduces a bare Context{} into v2/pkg/engine/resolve, but the package relies on constructor-initialized state. Switching to NewContext(context.Background()) keeps the test aligned with production setup and avoids future nil-map failures when the helper starts touching response/subgraph error state.

💡 Suggested change
 	runResolve := func(t *testing.T, opts ResolvableOptions, extensions ...string) string {
 		t.Helper()
 		res := NewResolvable(nil, opts)
-		ctx := &Context{}
+		ctx := NewContext(context.Background())
 		err := res.Init(ctx, []byte(`{"hello":"world"}`), ast.OperationTypeQuery)
 		assert.NoError(t, err)
Based on learnings Ensure all Context instances in `v2/pkg/engine/resolve` are created with `NewContext()` instead of `Context{}` literals.
🤖 Prompt for 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.

In `@v2/pkg/engine/resolve/resolvable_test.go` around lines 1433 - 1438, The
runResolve test helper creates a bare Context literal (Context{}) which bypasses
constructor-initialized state used elsewhere; update the helper to call
NewContext(context.Background()) instead of using Context{} so any maps/fields
are properly initialized before calling res.Init in NewResolvable; ensure the
test imports/uses context.Background() when constructing the Context via
NewContext(context.Background()) and run the same assertions after Init.
🤖 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 `@v2/pkg/engine/resolve/loader.go`:
- Around line 573-579: The extensions object from a subgraph response is being
ignored when the loader early-returns on null data; capture and record the
top-level extensions before that early return so error-related metadata isn't
lost. In loader.go, read response.Get("extensions") into a local variable and,
if l.allowCustomExtensionProperties and astjson.ValueIsNonNull(...)/TypeObject,
append it to l.resolvable.subgraphExtensions (same logic as the existing block)
before the code path that returns on null data; ensure you only append once by
removing or guarding the later append branch if you move the logic.

In `@v2/pkg/engine/resolve/resolvable.go`:
- Around line 459-467: getDefaultReservedExtensions currently reserves keys for
authorization, rateLimit, queryPlan, trace, and valueCompletion unconditionally;
change it to only include a key when the corresponding router/response extension
is actually enabled. Update getDefaultReservedExtensions to accept booleans (or
a config struct) indicating which extensions are enabled and only add
string(literalAuthorization), string(literalRateLimit),
string(literalQueryPlan), string(literalTrace), and
string(literalValueCompletion) when their flags are true, and then update all
callers (the other spots referenced around the function) to pass the right
enablement flags from the router/resolvable config so legitimate subgraph
extensions can be forwarded.
- Around line 69-70: The Resolvable struct's request-scoped fields
subgraphExtensions and allowedExtensions are not cleared between requests;
update Resolvable.Reset() (and/or Init()) to reset subgraphExtensions to nil (or
an empty slice) and allowedExtensions to nil (or make a new empty map) so
previously forwarded extensions are not reused across resolves; locate the
Resolvable type and ensure both Reset() and Init() clear or reinitialize these
fields to their zero-values.

---

Nitpick comments:
In `@v2/pkg/engine/resolve/resolvable_test.go`:
- Around line 1433-1438: The runResolve test helper creates a bare Context
literal (Context{}) which bypasses constructor-initialized state used elsewhere;
update the helper to call NewContext(context.Background()) instead of using
Context{} so any maps/fields are properly initialized before calling res.Init in
NewResolvable; ensure the test imports/uses context.Background() when
constructing the Context via NewContext(context.Background()) and run the same
assertions after Init.
🪄 Autofix (Beta)

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

Run ID: 6ba6c0b0-ec66-4510-89e5-a3c9a5a3b135

📥 Commits

Reviewing files that changed from the base of the PR and between 34d07fc and 7fa3765.

📒 Files selected for processing (5)
  • v2/pkg/engine/resolve/loader.go
  • v2/pkg/engine/resolve/loader_test.go
  • v2/pkg/engine/resolve/resolvable.go
  • v2/pkg/engine/resolve/resolvable_test.go
  • v2/pkg/engine/resolve/resolve.go

Comment thread v2/pkg/engine/resolve/loader.go
Comment thread v2/pkg/engine/resolve/resolvable.go
Comment thread v2/pkg/engine/resolve/resolvable.go

type ExtensionForwardingAlgorithm string

const (
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.

From what I understood without going too deep, this is for extracting extensions from subgraphs. You mention "Currently only a small set of extensions are allowed to be returned.". How are conflicts from these extensions handled right now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I ignore extensions that are pre-defined for our engine

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