Skip to content

feat(router): provide field arguments in operation context#2385

Open
dkorittki wants to merge 32 commits intomainfrom
dominik/eng-8582-support-access-to-field-arguments-in-cosmo-streams-hooks
Open

feat(router): provide field arguments in operation context#2385
dkorittki wants to merge 32 commits intomainfrom
dominik/eng-8582-support-access-to-field-arguments-in-cosmo-streams-hooks

Conversation

@dkorittki
Copy link
Contributor

@dkorittki dkorittki commented Dec 3, 2025

@CodeRabbit summary

Checklist

Motivation and Context

This pull request provides users of Cosmo Streams hooks the ability to access field arguments by their original name. Usually these get lost during the normalization of the operation, where for each argument the operation kit creates a variable:

subscription {
  employeeUpdatedMyKafka(employeeID:1) { # maps "employeeID" to variable "a"
    id
  }
}

In Cosmo Streams hooks, for example the OnReceiveEvents hook, you would need to access this argument by it's mapped variable "a":

func (m *CosmoStreamsModule) OnReceiveEvents(ctx core.StreamReceiveEventHandlerContext, events datasource.StreamEvents) (datasource.StreamEvents, error) {
	employeeID := ctx.Operation().Variables().GetInt("a")

	// do something with employeeID

	return events, nil
}

This is a problem because

  • The DX is bad
  • The naming scheme implies order of arguments, something which might change and could be easily forgotten to be adjusted here

One solution is to use variables in the operation:

subscription($id:Int!) {
  employeeUpdatedMyKafka(employeeID:$id) {
    id
  }
}

# vars:
# {
#   "id": 1
# }

This way one could access the values of an argument by accessing the variable id:

func (m *CosmoStreamsModule) OnReceiveEvents(ctx core.StreamReceiveEventHandlerContext, events datasource.StreamEvents) (datasource.StreamEvents, error) {
	employeeID := ctx.Operation().Variables().GetInt("id")

	// do something with employeeID

	return events, nil
}

But this assumes the user is in charge of its clients. This is not always the case, especially on public api's. Clients could choose all kinds of variable names a hook developer can't be aware of.

This leaves them only with the option to use the remapped variables, with all it's mentioned downsides.

Solution

Add Arguments() method to ctx.Operation() on custom modules interfaces of Cosmo Streams hooks. ctx.Operation() provides data of the current operation to hook developers and since field arguments are part of that operation, I think it fits well there.

type OperationContext interface {
	// [...]
	Variables() *astjson.Value
	Arguments() Arguments // this is new
}

Given an operation

subscription {
  employeeUpdatedMyKafka(employeeID:1) {
    id
  }
}

The hook developer can now access the field directly by its path and name

func (m *CosmoStreamsModule) OnReceiveEvents(ctx core.StreamReceiveEventHandlerContext, events datasource.StreamEvents) (datasource.StreamEvents, error) {
	employeeID := ctx.Operation().Arguments().Get("subscription.employeeUpdatedMyKafka.employeeID").GetInt()
	// employeeID is 1

	return events, nil
}

Arguments of nested fields are accessable by using a dot notated path of the field

subscription {
  rootfield(arg1:"val1") {
    subfield1
    subfield2(arg2: "val2", arg3: "val3")
  }
}
func (m *CosmoStreamsModule) OnReceiveEvents(ctx core.StreamReceiveEventHandlerContext, events datasource.StreamEvents) (datasource.StreamEvents, error) {
	arg2 := ctx.Operation().Arguments().Get("subscription.rootfield.subfield2.arg3").GetStringBytes()
        // [...]
}

ctx.Operation().Arguments().Get returns *astjson.Value objects. This way we can expose all kinds of different argument types, including GraphQL arrays and objects. It also makes using ctx.Operation().Arguments().Get() feel similar to ctx.Operation().Variables(), which also returns *astjson.Value objects. Have a look at the unit tests to see how it looks.

Implementation

Now we know how a hook developer can access field arguments but we how do we actually collect and provide them?

To implement this feature I added a new field to core.operationContext to hold the data as a map. The map is created during variable normalization, so it's ready to use when hooks call it. This implementation involves a change on the engine, where we extend the variable extraction visitor of the variable normalizer. Here is the pull request. That must be merged before we merge this one.

So the actual creation of the field argument map is done by the engine. At first the engine wasn't involved in this feature and the router did it itself with a new walker. After discussing this I changed this to the engine for performance reasons (see comments). The router does not have an existing walker which walks field arguments at the right place, so the data is ready when hooks are called. But the engine does have such a visitor: The variable normalizer. Doing it in the engine prevents us from using a dedicated visitor just for that.

After the engine change the variable normalizers method signature changes and returns the field arg mapping. We store this map on operationContext and that's what the ctx.Operation.Arguments() (used in hooks) acesses.

Caching

The results of the variable normalizer are cached for every operation. This is functionality that already exists. I added the field arg map to that cache as well. It's now part of the variable normalizer result and is cached with it.

The cache key generation did not change. The only two factors for a variable normalizer cache key are the operations variables and the normalized string representation. This is sufficient because if these factors do not change, the field argument mapping didn't as well.

Other Optimisations

The field arg mapping on the engine will only run if actual Cosmo Streams hooks are registered on the router. This is done via function needFieldArgumentMapping(). They are the only potential places of accessing field arguments this way, so it wouldn't make sense to process them otherwise.

Summary by CodeRabbit

  • New Features

    • Subscription and mutation hooks can now read GraphQL field arguments from the operation context when field-argument mapping is enabled (opt-in).
  • Tests

    • Added end-to-end and unit tests covering argument access for Start/Publish/Receive hooks and the argument-mapping utility, including rooted, nested, aliased, and variable-backed arguments.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

Adds an Arguments API and wires field-argument mapping through normalization, operation context, HTTP prehandler and WebSocket flows so hooks (start/publish/receive) can access GraphQL field arguments during operation execution. Includes unit and integration tests and a dependency pseudo-version update.

Changes

Cohort / File(s) Summary
Core Arguments API
router/core/arguments.go, router/core/arguments_test.go
New public Arguments type with NewArguments(...) and Get(path string) to resolve field-argument values from normalized variable data; comprehensive unit tests for argument access patterns and edge cases.
Operation Context
router/core/context.go
Adds Arguments() *Arguments to OperationContext, adds fieldArguments Arguments to operationContext and implements the accessor.
Variable Normalization & Processor
router/core/operation_processor.go, router/core/cache_warmup.go, router/core/operation_processor_test.go
Extend NormalizeVariables() to return fieldArgMapping (fourth return), add fieldArgumentMapping to VariablesNormalizationCacheEntry, and introduce EnableFieldArgumentMapping option propagated through parse/processor setup.
Request Pipeline: Prehandler & Graph Server
router/core/graphql_prehandler.go, router/core/graph_server.go
Add MapFieldArguments/EnableFieldArgumentMapping options; compute field-argument remapping during normalization and populate operation.fieldArguments when enabled; propagate flags into handler/middleware options.
WebSocket Flow
router/core/websocket.go
Add MapFieldArguments option to websocket middleware/handler/connection handler; during parse/plan, create opContext.fieldArguments when mapping is enabled.
Router Config / Hook Detection
router/core/router_config.go
Add needFieldArgumentMapping() on subscriptionHooks to enable mapping when hook handlers are present.
Integration Tests: Hooks
router-tests/modules/start_subscription_test.go, router-tests/modules/stream_publish_test.go, router-tests/modules/stream_receive_test.go
Add tests asserting StartSubscription, Publish, and Receive hooks can read field arguments via ctx.Operation().Arguments() during subscription/mutation flows (Kafka-enabled tests included).
Dependency Updates (tests & router)
router/go.mod, router-tests/go.mod
Bump github.com/wundergraph/graphql-go-tools/v2 to a pseudo-version commit (precise pseudo-version updated).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main change: adding field arguments accessibility to the operation context for use in hooks.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions bot added the router label Dec 3, 2025
@github-actions
Copy link

github-actions bot commented Dec 3, 2025

Router-nonroot image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-2099290d35066757e8bd70d084bc29e2c2eeb0a9-nonroot

@dkorittki
Copy link
Contributor Author

Important questions in my head:

  • Is it a good idea to create the map during operation parsing? I did it there to avoid it being created on-the-fly for hook developers, to keep accessing the map fast. But now we add some processing to the operation parsing which, depending on the complexity of the query, could take while. Haven't benchmarked this yet.
  • Is the way I access root field arguments on the AST document robust? Could this break?

@codecov
Copy link

codecov bot commented Dec 3, 2025

Codecov Report

❌ Patch coverage is 91.80328% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.68%. Comparing base (c8a6a5d) to head (4a28b25).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
router/core/operation_processor.go 78.26% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2385      +/-   ##
==========================================
+ Coverage   61.69%   62.68%   +0.98%     
==========================================
  Files         239      240       +1     
  Lines       25423    25464      +41     
==========================================
+ Hits        15685    15962     +277     
+ Misses       8418     8174     -244     
- Partials     1320     1328       +8     
Files with missing lines Coverage Δ
router/core/arguments.go 100.00% <100.00%> (ø)
router/core/cache_warmup.go 87.98% <100.00%> (-0.97%) ⬇️
router/core/context.go 73.35% <100.00%> (+0.16%) ⬆️
router/core/graph_server.go 84.18% <100.00%> (+1.36%) ⬆️
router/core/graphql_prehandler.go 83.90% <100.00%> (+0.71%) ⬆️
router/core/router_config.go 93.82% <100.00%> (+0.07%) ⬆️
router/core/websocket.go 76.13% <100.00%> (+0.26%) ⬆️
router/core/operation_processor.go 85.17% <78.26%> (+0.16%) ⬆️

... and 17 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dkorittki dkorittki changed the title feat: provide field arguments in operation context feat(router): provide field arguments in operation context Dec 12, 2025
@dkorittki
Copy link
Contributor Author

dkorittki commented Dec 12, 2025

I did a bunch of changes. We are now using a visitor to register all field arguments, including those of nested fields. Its only triggered when a user registers Cosmo Streams hooks. The User does not access the map directly but instead interfaces with a new type core.Argument. I will update the pull request description, which shows how it will be used.

@dkorittki dkorittki marked this pull request as ready for review December 12, 2025 09:26
Copy link
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
router/core/graphql_prehandler.go (1)

910-931: Fix potential nil-deref: mapFieldArguments runs before variables parse error check.

requestContext.operation.variables, err = variablesParser.ParseBytes(...) (Line 912) can return a non-nil err and a nil variables value; the code then calls mapFieldArguments(...) (Lines 914-923) which will dereference vars via .Get(...).

Suggested fix:

 requestContext.operation.rawContent = operationKit.parsedOperation.Request.Query
 requestContext.operation.content = operationKit.parsedOperation.NormalizedRepresentation
 requestContext.operation.variables, err = variablesParser.ParseBytes(operationKit.parsedOperation.Request.Variables)
-
- if h.mapFieldArguments {
-     requestContext.operation.fieldArguments = mapFieldArguments(mapFieldArgumentsOpts{
-         operation:      operationKit.kit.doc,
-         definition:     h.executor.ClientSchema,
-         vars:           requestContext.operation.variables,
-         remapVariables: requestContext.operation.remapVariables,
-         logger:         h.log,
-     })
- }
-
 if err != nil {
     rtrace.AttachErrToSpan(engineNormalizeSpan, err)
     if !requestContext.operation.traceOptions.ExcludeNormalizeStats {
         httpOperation.traceTimings.EndNormalize()
     }
     engineNormalizeSpan.End()
     return err
 }
+
+if h.mapFieldArguments {
+    requestContext.operation.fieldArguments = mapFieldArguments(mapFieldArgumentsOpts{
+        operation:      operationKit.kit.doc,
+        definition:     h.executor.ClientSchema,
+        vars:           requestContext.operation.variables,
+        remapVariables: requestContext.operation.remapVariables,
+        logger:         h.log,
+    })
+}
🧹 Nitpick comments (3)
router-tests/modules/stream_receive_test.go (1)

1016-1019: Remove duplicate type definition.

The kafkaSubscriptionArgs type is already defined at the file scope (lines 32-35). This inner redefinition is unnecessary and creates inconsistency with other tests in this file that use the file-scoped type.

-			type kafkaSubscriptionArgs struct {
-				dataValue []byte
-				errValue  error
-			}
-			subscriptionArgsCh := make(chan kafkaSubscriptionArgs)
+			subscriptionArgsCh := make(chan kafkaSubscriptionArgs)
router/core/field_argument_visitor.go (2)

53-64: Consider alias-aware (or otherwise disambiguated) field paths to prevent overwrites.

As-is, repeated fields like user / user (commonly disambiguated via aliases) will collide because dotPathFromAncestors() uses FieldNameString(...) only.

If the intent is “what the user typed”, consider preferring alias when present (pseudo-API names; adjust to actual graphql-go-tools helpers):

- fieldName := v.operation.FieldNameString(anc.Ref)
+ fieldName := v.operation.FieldNameString(anc.Ref)
+ if v.operation.FieldAliasIsDefined(anc.Ref) {
+     fieldName = v.operation.FieldAliasString(anc.Ref)
+ }

If you intentionally want schema field names, then at least document collision behavior in Arguments.Get(...) docs/tests.


106-135: Use (or remove) mapFieldArgumentsOpts.logger and check operationreport.Report errors.

Right now opts.logger is unused and report is ignored, so traversal errors can be silently dropped.

Suggested patch:

 func mapFieldArguments(opts mapFieldArgumentsOpts) Arguments {
@@
     visitor := &fieldArgumentsVisitor{
         walker:         &walker,
         variables:      opts.vars,
         remapVariables: opts.remapVariables,
         fieldArguments: make(map[string]map[string]*astjson.Value),
+        logger:         opts.logger,
     }
@@
     report := &operationreport.Report{}
     walker.Walk(opts.operation, opts.definition, report)
+    if report.HasErrors() {
+        if opts.logger != nil {
+            opts.logger.Debug("field-argument mapping walk reported errors", zap.String("report", report.Error()))
+        }
+        // Option A: return partial results (current behavior)
+        // Option B: return empty to avoid confusing partial state
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a53bb35 and b28d9a7.

📒 Files selected for processing (11)
  • router-tests/modules/start_subscription_test.go (1 hunks)
  • router-tests/modules/stream_publish_test.go (1 hunks)
  • router-tests/modules/stream_receive_test.go (1 hunks)
  • router/core/arguments.go (1 hunks)
  • router/core/context.go (3 hunks)
  • router/core/field_argument_visitor.go (1 hunks)
  • router/core/field_argument_visitor_test.go (1 hunks)
  • router/core/graph_server.go (2 hunks)
  • router/core/graphql_prehandler.go (4 hunks)
  • router/core/router_config.go (1 hunks)
  • router/core/websocket.go (8 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-11-13T10:10:47.680Z
Learnt from: dkorittki
Repo: wundergraph/cosmo PR: 2329
File: router/pkg/pubsub/datasource/subscription_event_updater.go:86-88
Timestamp: 2025-11-13T10:10:47.680Z
Learning: In router/pkg/pubsub/datasource/subscription_event_updater.go, the SetHooks method is intentionally designed to only replace hook handlers, not reconfigure timeout or semaphore settings. The timeout and semaphore fields are meant to be set once during construction via NewSubscriptionEventUpdater and remain immutable. If different timeout or concurrency settings are needed, a new updater instance should be created rather than modifying the existing one.

Applied to files:

  • router/core/router_config.go
📚 Learning: 2025-08-20T10:08:17.857Z
Learnt from: endigma
Repo: wundergraph/cosmo PR: 2155
File: router/core/router.go:1857-1866
Timestamp: 2025-08-20T10:08:17.857Z
Learning: router/pkg/config/config.schema.json forbids null values for traffic_shaping.subgraphs: additionalProperties references $defs.traffic_shaping_subgraph_request_rule with type "object". Therefore, in core.NewSubgraphTransportOptions, dereferencing each subgraph rule pointer is safe under schema-validated configs, and a nil-check is unnecessary.

Applied to files:

  • router/core/graph_server.go
📚 Learning: 2025-09-17T20:55:39.456Z
Learnt from: SkArchon
Repo: wundergraph/cosmo PR: 2172
File: router/core/graph_server.go:0-0
Timestamp: 2025-09-17T20:55:39.456Z
Learning: The Initialize method in router/internal/retrytransport/manager.go has been updated to properly handle feature-flag-only subgraphs by collecting subgraphs from both routerConfig.GetSubgraphs() and routerConfig.FeatureFlagConfigs.ConfigByFeatureFlagName, ensuring all subgraphs receive retry configuration.

Applied to files:

  • router/core/graph_server.go
  • router/core/graphql_prehandler.go
📚 Learning: 2025-09-02T12:52:27.677Z
Learnt from: endigma
Repo: wundergraph/cosmo PR: 2181
File: router/core/operation_processor.go:0-0
Timestamp: 2025-09-02T12:52:27.677Z
Learning: Hash validation for persisted queries with query bodies is performed in router/core/graphql_prehandler.go in the handleOperation function, not in the APQ processing logic in operation_processor.go. This validates that extensions.persistedQuery.sha256Hash matches the query body before any APQ operations occur.

Applied to files:

  • router/core/graphql_prehandler.go
📚 Learning: 2025-09-02T12:52:27.677Z
Learnt from: endigma
Repo: wundergraph/cosmo PR: 2181
File: router/core/operation_processor.go:0-0
Timestamp: 2025-09-02T12:52:27.677Z
Learning: Hash validation for persisted queries with query bodies is performed in router/core/graphql_prehandler.go in the handleOperation function at lines 571-578, not in the APQ processing logic in operation_processor.go. This validates that extensions.persistedQuery.sha256Hash matches the computed query hash before any APQ operations occur. There's also a test case that verifies this behavior.

Applied to files:

  • router/core/graphql_prehandler.go
🧬 Code graph analysis (4)
router-tests/modules/stream_receive_test.go (2)
router/core/subscriptions_modules.go (1)
  • StreamReceiveEventHandlerContext (269-292)
router/core/arguments.go (1)
  • Arguments (6-11)
router-tests/modules/start_subscription_test.go (4)
router-tests/modules/start-subscription/module.go (1)
  • StartSubscriptionModule (14-19)
router/core/subscriptions_modules.go (1)
  • SubscriptionOnStartHandlerContext (15-53)
router/core/arguments.go (1)
  • Arguments (6-11)
router/core/router_config.go (1)
  • Config (53-148)
router-tests/modules/stream_publish_test.go (3)
router-tests/modules/stream-publish/module.go (1)
  • PublishModule (14-18)
router/core/subscriptions_modules.go (1)
  • StreamPublishEventHandlerContext (306-327)
router/core/arguments.go (1)
  • Arguments (6-11)
router/core/context.go (1)
router/core/arguments.go (1)
  • Arguments (6-11)
🔇 Additional comments (13)
router/core/router_config.go (1)

49-51: LGTM!

The needFieldArgumentMapping() method correctly determines if field argument mapping is needed by checking if any subscription hook handlers are registered. The implementation is simple and efficient - len() on nil slices safely returns 0.

router-tests/modules/stream_receive_test.go (1)

967-1051: Test correctly validates field argument access in receive hooks.

The test properly verifies that GraphQL field arguments can be accessed via ctx.Operation().Arguments(). The synchronization through WaitForSubscriptionCount, message production, channel awaiting, and client.Close() ensures that capturedEmployeeID is written before being read in the assertion.

router-tests/modules/stream_publish_test.go (1)

362-408: LGTM!

The test correctly validates that publish hooks can access GraphQL field arguments via ctx.Operation().Arguments(). Since the mutation is synchronous, the callback executes and completes before MakeGraphQLRequestOK returns, ensuring proper sequencing of the write and read of capturedEmployeeID.

router/core/graph_server.go (2)

1473-1473: LGTM!

The MapFieldArguments flag is correctly derived from needFieldArgumentMapping() and passed to the PreHandler, enabling field argument mapping for HTTP-based GraphQL requests when subscription hooks are registered.


1499-1499: LGTM!

Consistent propagation of MapFieldArguments to WebSocket middleware ensures field arguments are available for WebSocket-based subscriptions as well.

router/core/arguments.go (1)

1-43: LGTM!

The Arguments type provides a clean, nil-safe API for accessing GraphQL field arguments. Key design decisions are sound:

  • Nil receiver check prevents panics
  • Unexported data field ensures immutability from consumers
  • Map lookups safely return nil for missing keys
  • Documentation clearly explains the dot-notation path convention
router-tests/modules/start_subscription_test.go (1)

715-785: LGTM!

The test correctly validates that subscription start hooks can access GraphQL field arguments via ctx.Operation().Arguments(). The synchronization through WaitForSubscriptionCount ensures the callback (which captures employeeID) executes before the assertion runs.

router/core/field_argument_visitor_test.go (1)

15-289: LGTM!

Excellent test coverage for the mapFieldArguments function. The test cases comprehensively cover:

  • Root field arguments (with and without variables)
  • Nested field arguments with dot-notation paths
  • Non-existent fields/arguments returning nil
  • Multiple root fields in a single operation
  • Array and object argument types

The test setup correctly mirrors the router's normalization pipeline, ensuring the tests validate realistic scenarios.

router/core/context.go (2)

485-488: LGTM!

The Arguments() method addition to the OperationContext interface follows the existing pattern established by Variables(). The documentation comment appropriately describes the purpose.


564-566: LGTM!

The implementation correctly returns a pointer to the embedded fieldArguments field. This is safe because:

  1. The operationContext outlives any usage of the returned *Arguments
  2. The zero value of Arguments (nil data map) is handled gracefully by Arguments.Get() which returns nil
router/core/graphql_prehandler.go (1)

42-75: Good feature-flag plumbing for field-arguments mapping.

Also applies to: 77-111, 133-171

router/core/websocket.go (2)

45-91: Wiring MapFieldArguments through the WS stack looks consistent.

Also applies to: 241-273, 696-721, 722-762, 775-802


824-978: Schema source is consistent; both HTTP and WebSocket paths use the same executor instance.

Both PreHandler.executor and OperationProcessor.executor are initialized from the same executor variable in graph_server.go (lines 1275 and 1446). Therefore, h.executor.ClientSchema (HTTP, line 917) and h.operationProcessor.executor.ClientSchema (WebSocket, line 943) reference identical schema sources—no divergence possible.

This test verifies that values of arguments can be
extracted directly from the AST when no variable
remapping has taken place. Usually this is not
the case in reality but if for whatever reason it
will be reality one day, this method is proven to
work with that.
Copy link
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

♻️ Duplicate comments (2)
router/core/field_argument_visitor.go (2)

29-34: Fix possible panic: Ancestors[len-1] without length check.

v.walker.Ancestors[len(v.walker.Ancestors)-1] will panic when Ancestors is empty (even if “unlikely”, visitors can be invoked in unexpected contexts).

 func (v *fieldArgumentsVisitor) EnterArgument(ref int) {
 	// skip if we don't deal with field arguments (e.g. directive arguments)
+	if len(v.walker.Ancestors) == 0 {
+		return
+	}
 	anc := v.walker.Ancestors[len(v.walker.Ancestors)-1]
 	if anc.Kind != ast.NodeKindField {
 		return
 	}

97-107: Handle nil v.variables explicitly (and decide missing-vs-null semantics).

v.variables.Get(...) will panic when opts.vars is nil. Even if “router always sets {}”, this is an easy footgun in other call paths.

 func (v *fieldArgumentsVisitor) getArgValueFromVars(val ast.Value) (*astjson.Value, error) {
+	if v.variables == nil {
+		return nil, fmt.Errorf("no variables supplied")
+	}
 	varName := v.operation.VariableValueNameString(val.Ref)
 	originalVarName := varName
 	if v.remapVariables != nil {
 		if original, ok := v.remapVariables[varName]; ok {
 			originalVarName = original
 		}
 	}
 
 	return v.variables.Get(originalVarName), nil
 }

Also worth clarifying behavior: today Get(...) == nil conflates “variable missing” and “variable is null” (and callers can’t distinguish).

🧹 Nitpick comments (1)
router/core/field_argument_visitor_test.go (1)

350-355: Test brittleness: avoid asserting “exactly 1 warning log”.

require.Len(t, logs, 1) will fail if future code logs an additional warn (even if behavior is correct). Prefer asserting “at least one warn” and matching on the message/fields you care about.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b28d9a7 and d6fcf63.

📒 Files selected for processing (2)
  • router/core/field_argument_visitor.go (1 hunks)
  • router/core/field_argument_visitor_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
router/core/field_argument_visitor_test.go (1)
router/core/arguments.go (1)
  • Arguments (6-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: lint
  • GitHub Check: lint
  • GitHub Check: lint
  • GitHub Check: build-router
  • GitHub Check: integration_test (./. ./fuzzquery ./lifecycle ./modules)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: integration_test (./events)
  • GitHub Check: build_test
  • GitHub Check: build_push_image (nonroot)
  • GitHub Check: image_scan
  • GitHub Check: image_scan (nonroot)
  • GitHub Check: integration_test (./telemetry)
  • GitHub Check: build_push_image
  • GitHub Check: Analyze (go)

Copy link
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: 1

♻️ Duplicate comments (2)
router/core/field_argument_visitor.go (2)

61-72: Alias collision issue from previous review remains unresolved.

As noted in the earlier review, dotPathFromAncestors uses only the field name (via FieldNameString), so selections with aliases like a: user(id: 1) and b: user(id: 2) both map to "user" and overwrite each other. This causes last-write-wins behavior and prevents hooks from accessing arguments for aliased fields independently.

Consider incorporating the response key (alias if present, otherwise field name) into the path, or append the ancestor ref to disambiguate:

func (v *fieldArgumentsVisitor) dotPathFromAncestors() string {
	var pathParts []string
	for _, anc := range v.walker.Ancestors {
		if anc.Kind == ast.NodeKindField {
			// Use alias if present, otherwise field name
			alias := v.operation.FieldAliasString(anc.Ref)
			if alias == "" {
				alias = v.operation.FieldNameString(anc.Ref)
			}
			pathParts = append(pathParts, alias)
		}
	}
	return strings.Join(pathParts, ".")
}

If this behavior is intentional, document it clearly.


145-146: Minor: Walker errors are silently ignored.

As flagged in the previous review, walker.Walk can accumulate errors in report, but they're never checked or logged. This can make debugging difficult when field arguments are unexpectedly empty or incomplete.

Add error logging without changing the return signature:

 	report := &operationreport.Report{}
 	walker.Walk(opts.operation, opts.definition, report)
+	
+	if report.HasErrors() {
+		logger.Debug("walker encountered errors while mapping field arguments",
+			zap.String("errors", report.String()),
+		)
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6fcf63 and cdca088.

📒 Files selected for processing (2)
  • router/core/field_argument_visitor.go (1 hunks)
  • router/core/graphql_prehandler.go (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-17T20:55:39.456Z
Learnt from: SkArchon
Repo: wundergraph/cosmo PR: 2172
File: router/core/graph_server.go:0-0
Timestamp: 2025-09-17T20:55:39.456Z
Learning: The Initialize method in router/internal/retrytransport/manager.go has been updated to properly handle feature-flag-only subgraphs by collecting subgraphs from both routerConfig.GetSubgraphs() and routerConfig.FeatureFlagConfigs.ConfigByFeatureFlagName, ensuring all subgraphs receive retry configuration.

Applied to files:

  • router/core/graphql_prehandler.go
🧬 Code graph analysis (1)
router/core/field_argument_visitor.go (1)
router/core/arguments.go (1)
  • Arguments (6-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: build-router
  • GitHub Check: integration_test (./events)
  • GitHub Check: integration_test (./. ./fuzzquery ./lifecycle ./modules)
  • GitHub Check: build_push_image (nonroot)
  • GitHub Check: image_scan
  • GitHub Check: image_scan (nonroot)
  • GitHub Check: integration_test (./telemetry)
  • GitHub Check: build_push_image
  • GitHub Check: build_test
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
🔇 Additional comments (4)
router/core/graphql_prehandler.go (2)

70-70: LGTM: Feature flag wiring is clean.

The MapFieldArguments option is correctly threaded through PreHandlerOptions, stored in PreHandler, and initialized in NewPreHandler. The naming and pattern are consistent with other feature flags in this struct.

Also applies to: 106-106, 165-165


923-931: LGTM: Field argument mapping correctly placed in normalization flow.

The conditional call to mapFieldArguments is appropriately positioned after variable parsing and remapping succeed, ensuring the visitor has access to fully normalized variables. The parameters passed (operation document, schema, parsed variables, remap map, logger) align with what the visitor needs.

router/core/field_argument_visitor.go (2)

30-59: LGTM: Previous safety concerns addressed.

The empty Ancestors check (lines 31-33) prevents the panic that was flagged in earlier review, and error logging (lines 52-54) now provides visibility into resolution failures with field path and argument name context.


102-116: LGTM: Nil variables check added.

The nil-variables guard (lines 103-105) addresses the previous review concern. Note that Get returns nil for both missing and explicitly-null variables—this is acceptable for the hook use case, though it means hooks cannot distinguish between "argument not provided" and "argument is null."

WebSocketConfiguration: s.webSocketConfiguration,
ClientHeader: s.clientHeader,
DisableVariablesRemapping: s.engineExecutionConfiguration.DisableVariablesRemapping,
MapFieldArguments: s.subscriptionHooks.needFieldArgumentMapping(),
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be good to also have a disable flag to prevent the visitor from running in general.

Copy link
Contributor Author

@dkorittki dkorittki Dec 22, 2025

Choose a reason for hiding this comment

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

Yeah it probably makes sense.

Would you make this part of the router configuration or part of Custom Modules code, when you define them?

An alternative would be that I hook myself into a visitor, which already visits arguments. I could save a seperate walk here. I believe there was one visitor, which already does that but need to check if it is viable. What do you think of that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As now we don't use a dedicated walker anymore (runs inside the engines variable normalizer walker) I think we can omit a new config flag. If someone wants to disable it actively we can always add it afterwards.

@dkorittki dkorittki marked this pull request as draft January 27, 2026 13:14
@dkorittki
Copy link
Contributor Author

I set this pull request back to draft. Currently I'm working on another approach which avoids a seperate walker entirely.
It will involve engine changes and once both pull requests (this and the engine one) will be ready I will reopen this one.

Copy link
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: 1

🤖 Fix all issues with AI agents
In `@router/core/operation_processor.go`:
- Line 1443: The call to astnormalization.NewVariablesNormalizer currently
passes options.enableFieldArgumentMapping but the constructor takes no
arguments; update the variablesNormalizer initialization to call
astnormalization.NewVariablesNormalizer() with no parameters (remove
options.enableFieldArgumentMapping) so the variablesNormalizer variable is
created by NewVariablesNormalizer() as intended.
🧹 Nitpick comments (4)
router/core/graphql_prehandler.go (1)

824-825: Misplaced comment — the "storing" happens 120 lines later at lines 944–949.

The comment on line 824 says "Store the field argument mapping for later use when creating Arguments" but the actual assignment to requestContext.operation.fieldArguments happens much later (lines 944–949). This comment would be less confusing if moved to where the storing actually occurs, or removed here entirely.

Suggested fix
-	// Store the field argument mapping for later use when creating Arguments
 	engineNormalizeSpan.SetAttributes(otel.WgVariablesNormalizationCacheHit.Bool(cached))

And at line 943:

+	// Build field arguments from the mapping produced during variable normalization
 	if h.mapFieldArguments {
router-tests/modules/stream_receive_test.go (1)

1016-1019: Remove duplicate kafkaSubscriptionArgs type declaration.

This type is already declared at lines 32-35 in the outer TestReceiveHook scope with the same fields. The inner redeclaration shadows it unnecessarily.

♻️ Suggested fix
-			type kafkaSubscriptionArgs struct {
-				dataValue []byte
-				errValue  error
-			}
-			subscriptionArgsCh := make(chan kafkaSubscriptionArgs)
+			subscriptionArgsCh := make(chan kafkaSubscriptionArgs)
router/core/arguments_test.go (1)

330-340: Consider adding a nil-receiver test for Arguments.Get.

Per the Arguments.Get implementation, it handles a nil receiver. A test like (*Arguments)(nil).Get("path") would verify that safety net, complementing the nil/empty mapping tests here.

🧪 Suggested additional test
func TestArguments_Get_NilReceiver(t *testing.T) {
	var args *Arguments
	assert.Nil(t, args.Get("query.user.id"))
}
router/core/graph_server.go (1)

1313-1313: Store needFieldArgumentMapping() result in a local variable to avoid redundant computation.

s.subscriptionHooks.needFieldArgumentMapping() is called three times in buildGraphMux within different struct initializations. Since it's a pure function with no side effects, storing the result once improves readability.

♻️ Suggested refactor

Add near the top of the struct initializations:

+	needFieldArgMapping := s.subscriptionHooks.needFieldArgumentMapping()

Then replace the three call sites:

-		EnableFieldArgumentMapping:                       s.subscriptionHooks.needFieldArgumentMapping(),
+		EnableFieldArgumentMapping:                       needFieldArgMapping,
-		MapFieldArguments:                      s.subscriptionHooks.needFieldArgumentMapping(),
+		MapFieldArguments:                      needFieldArgMapping,
-			MapFieldArguments:         s.subscriptionHooks.needFieldArgumentMapping(),
+			MapFieldArguments:         needFieldArgMapping,

@dkorittki dkorittki marked this pull request as ready for review February 10, 2026 10:48
@dkorittki
Copy link
Contributor Author

Refactored the pull request description with better structure and more context.

@dkorittki
Copy link
Contributor Author

dkorittki commented Feb 23, 2026

Awaiting a new review here after adressing feedback. Simultaneously awaiting an approval on wundergraph/graphql-go-tools#1373 . I need to merge both in close order, first the engine and then the router. The engine PR is a breaking change as it changes a method signature used in router. This PR adresses this. So once this is approved I keep the PR open and updated until the engine PR has been approved, merged and released. Once that happened I will update go.mod here to use latest engine version.

@dkorittki
Copy link
Contributor Author

@CodeRabbit summary

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

✅ Actions performed

Summary regeneration triggered.

Copy link
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@router/core/arguments_test.go`:
- Around line 520-538: The test sets operation.Input.Variables to
[]byte(tc.variables) which becomes empty bytes when tc.variables == "" and
causes astjson.ParseBytes to fail; change the setup so when tc.variables is
empty you assign operation.Input.Variables = []byte("{}") (i.e. normalize
missing variables to an empty JSON object) before calling
astnormalization.NewNormalizer/astnormalization.NewVariablesNormalizer and
astjson.ParseBytes; update the code around operation.Input.Variables,
VariablesNormalizerOptions/varNorm.NormalizeOperation and the following
astjson.ParseBytes call to use this non-empty "{}" fallback.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac1ecc9 and 75244d3.

⛔ Files ignored due to path filters (2)
  • router-tests/go.sum is excluded by !**/*.sum
  • router/go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • router-tests/go.mod
  • router/core/arguments_test.go
  • router/core/context.go
  • router/core/graph_server.go
  • router/core/graphql_prehandler.go
  • router/core/operation_processor.go
  • router/core/operation_processor_test.go
  • router/core/router_config.go
  • router/go.mod
🚧 Files skipped from review as they are similar to previous changes (4)
  • router/go.mod
  • router/core/router_config.go
  • router-tests/go.mod
  • router/core/operation_processor_test.go

@@ -295,7 +323,7 @@ func (c *CacheWarmupPlanningProcessor) ProcessOperation(ctx context.Context, ope
return nil, err
}

_, _, err = k.NormalizeVariables()
_, _, _, err = k.NormalizeVariables()
Copy link
Contributor

Choose a reason for hiding this comment

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

nice 🐕 🛷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants