Skip to content

feat(router): subgraph header groups (headers.groups) — alternative to #2868#2869

Open
mwisner wants to merge 2 commits into
wundergraph:mainfrom
mwisner:feat/router-header-subgraph-groups
Open

feat(router): subgraph header groups (headers.groups) — alternative to #2868#2869
mwisner wants to merge 2 commits into
wundergraph:mainfrom
mwisner:feat/router-header-subgraph-groups

Conversation

@mwisner
Copy link
Copy Markdown

@mwisner mwisner commented May 14, 2026

AI disclosure: This PR was drafted with assistance from an AI coding agent (opencode + Claude). All code, tests, and docs were reviewed by a human before submission. Flagging this so reviewers can apply appropriate scrutiny.

Alternative proposal — see also #2868: This PR and #2868 solve the same problem with different API surface area. Either can land; they're not meant to ship together. See the comparison table below.

Why

Subgraph-specific header rules in the router are matched by exact subgraph name today. That works well when each subgraph's rules are unique, but it doesn't scale cleanly to two common cases:

  1. A cohort of subgraphs sharing the same rules. Many graphs have a large set of subgraphs that need the same forwarding rules (e.g. propagate Authorization, propagate X-User-Id, etc.). Today you either:

    • Move the rule to headers.all, which applies to every subgraph regardless of need, or
    • Duplicate the same rule block under each subgraph name in headers.subgraphs. This is verbose, drifts over time, and motivates external preprocessors that compile a compact authoring format into the verbose router config.
  2. Feature subgraphs replacing a base subgraph. A feature subgraph is composed under its own name (e.g. products-feature-v2), not the base name (products), so per-subgraph rules defined for products do not apply to its feature variants. This forces config edits and a router redeploy each time a feature subgraph is created or renamed.

This PR adds a single, opt-in primitive that handles both cases: named header rule groups with a selector that can be an explicit subgraph list, a regex, or both.

What

A new optional headers.groups list. Each entry bundles request/response header rules with a selector that picks the subgraphs it applies to.

Schema example

headers:
  all:
    request:
      - op: propagate
        named: X-Request-Id

  groups:
    # Pure list — a stable cohort of subgraphs.
    - id: authenticated
      subgraphs: [users, products, reviews, inventory]
      request:
        - op: propagate
          matching: (?i)^authorization$
        - op: propagate
          named: X-User-Id

    # Pure regex — a dynamic cohort (e.g. PR-preview feature subgraphs).
    - id: feature-previews
      matching: "^.+-feature-.+$"
      request:
        - op: propagate
          named: X-Feature-Preview-Token

    # Hybrid — base subgraph plus its feature variants in one rule.
    - id: products-cohort
      subgraphs: [products]
      matching: "^products-feature-.+$"
      request:
        - op: propagate
          named: X-Products-Auth
      response:
        - op: propagate
          algorithm: last_write
          named: X-Products-Trace

  # Existing fields still work, unchanged.
  subgraphs:
    products:
      request:
        - op: set
          name: X-Products-Internal
          value: yes

A subgraph matches a group if it appears in the group's subgraphs list or its name matches the group's matching regex (the union of the two). negate_match: true only inverts the regex result; subgraphs in the explicit list are always included positively.

Rule evaluation order

For each subgraph the router builds a header set in this order:

  1. headers.all rules.
  2. headers.groups entries whose selector matches the subgraph (in config order).
  3. headers.subgraphs.<name> rules for the exact subgraph name.

Exact-name rules apply last so they can still override a broader group rule (e.g. via op: set). This is the same precedence rule that exists between all and subgraphs.<name> today.

A group fires at most once per subgraph, even when both the explicit list and the regex would match it.

Conflict resolution

When more than one rule targets the same header name for the same subgraph, the router applies rules in a strict deterministic order and the last writer wins per header name. There is no warning, no error, and no validator that detects overlapping rules.

The full ordering across layers:

  1. headers.all rules (in declared order).
  2. headers.groups entries whose selector matches the subgraph, in config order. Within each group, rules apply in declared order.
  3. headers.subgraphs.<name> rules (in declared order).

Concretely, within the groups layer, group order in the YAML is the tiebreaker — entries are merged first-to-last, and a later group's rule overrides an earlier group's rule for the same header name. The headers.subgraphs.<name> layer always runs last and overrides every group rule for that exact subgraph.

There is one subtle asymmetry between operations: an op: propagate rule with no client-supplied value and no default: is a no-op and does not clear an earlier value. An op: set rule, and an op: propagate rule with a default: set, always write a value. This behavior matches headers.subgraphs.<name> today and is locked in by the new TestBuildRequestHeaderForSubgraph_GroupsPrecedence test cases.

# Two groups targeting X-Foo on the same subgraph
headers:
  groups:
    - id: first
      subgraphs: [products]
      request:
        - op: set
          name: X-Foo
          value: from-first
    - id: second                # second group runs after first
      subgraphs: [products]
      request:
        - op: set
          name: X-Foo
          value: from-second    # → wins among groups; X-Foo == "from-second"
  subgraphs:
    products:
      request:
        - op: set
          name: X-Foo
          value: from-exact     # → wins overall; exact runs last

Validation (at startup, not request time)

  • id is required and must be unique across groups.
  • At least one of subgraphs / matching must be set (otherwise the group would never apply).
  • At least one of request / response rules must be set (otherwise the group has no effect).
  • matching, when present, must be a valid Go regex.
  • Empty subgraph names in the list are rejected.

Misconfiguration fails router init with a clear error message rather than producing wrong header sets at request time.

Backward compatibility

No breaking changes.

  • headers.groups is a new optional field. Configs without it behave identically to today.
  • headers.all and headers.subgraphs.<name> continue to work with no semantic change.
  • No proto / router execution config changes — entirely router-local.
  • A fast-path skips all group logic when headers.groups is empty (len(rules.Groups) == 0), so existing users see zero added work per request.

Performance

The hot path is BuildRequestHeaderForSubgraph, called per data source in the execution plan via SubgraphHeadersBuilder.

Configuration Cost added by this PR
No headers.groups None — the indexing structures are nil and the helper returns immediately
g groups, list-only O(1) per subgraphsubgraphToGroupIdx map lookup
g groups, regex-only One regex.MatchString per regex group
g groups, hybrid List lookup first (O(1)), then regex for groups not already matched

Concrete properties:

  • Regexes compiled once at startup in NewHeaderPropagation, stored in groupRegex []*regexp.Regexp index-aligned with rules.Groups. Invalid regexes fail router init.
  • Inverse index subgraphToGroupIdx map[string][]int built once at startup turns "which groups list this subgraph?" into a single map read.
  • No ReDoS risk — Go's regexp uses RE2, which is linear-time and rejects backtracking constructs.
  • No per-match allocations for typical subgraph names.
  • Single-fire guarantee: when both list and regex would match, the group's rules apply only once. Implemented via a per-call []bool of len(rules.Groups) (small fixed allocation).

For realistic configs (g ≤ 10, plan touching k ≤ 20 subgraphs, names ~30 chars), the added cost is dominated by map lookups and is sub-microsecond per built header set.

Comparison with #2868 (subgraph_patterns)

Dimension #2868 (subgraph_patterns) This PR (headers.groups)
Selector shapes Regex only List, regex, or both
Solves PR-preview / feature-subgraph use case Yes Yes
Solves "rules-once-applied-to-many cohort" Only when names share a regex Yes (explicit list)
Replaces external preprocessors that expand cohorts No Yes
List-membership lookup performance n/a O(1) per subgraph
Regex match performance One RE2 per pattern One RE2 per regex group
API surface area One field One field
Implementation size ~140 lines + tests ~260 lines + tests
Group identity (id) for traceability No Yes
Backward compatible Yes Yes

headers.groups is a strict superset of subgraph_patterns (a regex-only group { matching: ... } is equivalent to a pattern). Reviewers may prefer the smaller, more focused API in #2868, or the richer, more expressive API in this PR. This PR's branch is independent of #2868 — it does not stack on it and does not assume it lands.

Implementation summary

File Change
router/pkg/config/config.go New SubgraphHeaderGroup type and HeaderRules.Groups field
router/core/header_rule_engine.go Validate + index groups at startup; new forEachMatchingGroup helper that combines list lookup and regex evaluation; wire group rules into request and response paths between all and exact-name; extend hasRequestRulesForSubgraph and SubgraphRules
router/pkg/config/config.schema.json Schema entry under headers.groups
router/pkg/config/fixtures/full.yaml Add list/regex/hybrid groups example to the full-config fixture
router/pkg/config/testdata/config_*.json Updated golden fixtures for the new field
router/core/header_rule_engine_test.go Response-side group test (list, regex, non-matching)
router/core/header_rule_engine_buildheader_test.go Request-side group tests: list-only, regex-only, hybrid (list path, regex path, non-matching, single-fire), negate_match regex-only inversion, evaluation order across all → multiple groups → exact, all six startup validation paths, no-groups fast-path, SubgraphRules helper coverage. Plus TestBuildRequestHeaderForSubgraph_GroupsPrecedence (9 subtests) covering conflict resolution between groups, mixed set/propagate collisions, propagate-without-default no-op behavior, intra-group rule order, and exact-name override.
docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx Example, evaluation order, group reference section, Conflict resolution subsection
docs-website/router/proxy-capabilities/subgraph-response-header-operations.mdx Example and explanation paragraph linking to the request-side reference

Validation

Locally on darwin/arm64 with Go 1.26.3:

go build ./...                        # clean
go vet ./core/... ./pkg/config/...    # clean
go test ./core/                       # PASS (incl. all new group tests + 9 precedence subtests)
go test ./pkg/config/                 # PASS (golden fixtures updated)

Notes for reviewers

  • Naming: I went with groups (under headers.) to keep scoping clean. Open to subgraph_groups if reviewers prefer.
  • Validation is intentionally strict at startup. I'd rather fail to boot with a clear error than silently produce a config that doesn't match operator intent.
  • Conflict resolution is purely deterministic config order, last-writer-wins. No warnings or errors are emitted for overlapping rules — this matches existing headers.subgraphs.<name> behavior. If reviewers want a "warn on overlapping rules at startup" pass, that's an easy follow-up; left out so the diff stays focused.
  • One follow-up I deliberately did not include: emitting the group id as an attribute on subgraph spans / metrics. Easy to add later; left out so the diff stays focused on the header-propagation behavior.
  • If reviewers prefer the smaller surface area in feat(router): regex selectors for subgraph header rules (subgraph_patterns) #2868, this PR is fine to close. The branch is isolated and does not depend on or block feat(router): regex selectors for subgraph header rules (subgraph_patterns) #2868.

Summary by CodeRabbit

  • New Features

    • Added support for header rule groups to apply header operations to multiple subgraphs using explicit lists, regex matching, or a combination of both. Groups are evaluated in configuration order after global rules and before subgraph-specific rules.
  • Documentation

    • Updated subgraph request and response header operations documentation with headers.groups configuration, precedence rules, selector options, and practical examples.

Review Change Stack

Adds an ordered `headers.groups` list whose entries bundle a set of
request/response header rules with a selector. The selector can be an
explicit list of subgraph names (`subgraphs:`), a Go regex against the
subgraph name (`matching:`), or both (a subgraph qualifies if it
matches either). Groups apply between `headers.all` and exact
`headers.subgraphs.<name>` rules, so an exact-name rule can still
override a group rule.

Each group requires a unique `id`, at least one of `subgraphs`/
`matching`, and at least one of `request`/`response` rules. Selector
regexes are compiled once at startup; an explicit subgraph -> group
index inverse map makes list-based matching O(1) at request time.
Existing configs without `headers.groups` hit a fast-path and pay no
additional cost.

This is an alternative API surface to the regex-only `subgraph_patterns`
proposal; both target the same use case (rules-once-applied-to-many).
Reviewers can pick either approach.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Walkthrough

This PR adds support for header rule groups in the router's header propagation engine. Groups enable applying header rules to multiple subgraphs by explicit list and/or regex pattern, evaluated in config order between global and per-subgraph rules. The implementation includes precomputed group indices, validation, matching logic, and comprehensive test coverage.

Changes

Header Rule Groups Implementation

Layer / File(s) Summary
Config Types & Schema
router/pkg/config/config.go, router/pkg/config/config.schema.json
New SubgraphHeaderGroup type with ID, Subgraphs, Matching, NegateMatch, Request, Response fields; added Groups ordered list to HeaderRules; JSON schema defines group structure and composable rule arrays.
Group Initialization & Validation
router/core/header_rule_engine.go
HeaderPropagation gains precomputed group fields (compiled regexes, subgraph-to-group index, presence booleans); NewHeaderPropagation calls indexGroups() to validate IDs, selectors, rules, and compile regexes at startup.
Header Rule Collection & Matching
router/core/header_rule_engine.go
getAllRules() aggregates from rules.Groups; BuildRequestHeaderForSubgraph() applies matching group request rules (config order) via new forEachMatchingGroup() helper that ORs explicit list and regex matches with deduplication; hasRequestRulesForSubgraph() uses precomputed indices and regex.
Response Rules & Rule Metadata
router/core/header_rule_engine.go
ApplyResponseHeaderRules() applies matching group response rules (config order); SubgraphRules() now includes group request rules in evaluation order, handling both explicit and regex matches with optional negation.
Request Header Building Tests
router/core/header_rule_engine_buildheader_test.go
Tests cover list-only, regex-only, and hybrid group selection; negate_match semantics; evaluation order across All → groups → exact subgraph rules; startup validation (missing IDs, duplicates, empty selectors/rules, invalid regex); and no-groups regression.
Response Header Tests & Fixtures
router/core/header_rule_engine_test.go, router/pkg/config/fixtures/full.yaml, router/pkg/config/testdata/config_*.json
Response group propagation test with context-keyed metadata; three group patterns (list-only, regex-only, hybrid) in YAML and JSON fixtures demonstrating combined selector and rule application.
Documentation
docs-website/router/proxy-capabilities/subgraph-*.mdx
User guide explaining group configuration, selector field semantics, validation constraints, rule evaluation order, and precedence relative to global and exact-subgraph rules.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately describes the primary change: adding optional headers.groups support to the router configuration as an alternative approach to issue #2868.
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.

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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

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.

🧹 Nitpick comments (8)
docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx (4)

72-73: ⚡ Quick win

Remove LLM-like justification.

The phrase "This is useful for" sounds promotional rather than factual. State what groups do, not why they are useful. As per coding guidelines, avoid phrases like "This powerful feature allows you to..." and describe what things do, not how impressive they are.

✍️ Suggested revision
-The `groups` section applies a bundle of rules to every subgraph that matches the group's selector. A group can list subgraphs explicitly (`subgraphs:`), match by regex (`matching:`), or both. This is useful for applying the same rules to a base subgraph and its feature variants without duplicating the same block under every entry.
+The `groups` section applies a bundle of rules to every subgraph that matches the group's selector. A group can list subgraphs explicitly (`subgraphs:`), match by regex (`matching:`), or both.
🤖 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
`@docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx`
around lines 72 - 73, Remove the promotional phrasing in the paragraph about
groups — specifically delete the sentence starting with "This is useful for
applying..." and replace it with a factual statement describing behavior only:
that a group applies the listed bundle of rules to every subgraph matching the
group's selector and can target subgraphs either via explicit subgraphs:
entries, via matching: regex patterns, or both (referencing "groups",
"subgraphs:", and "matching:"). Keep the explanation terse and factual without
value-laden words.

97-97: ⚡ Quick win

Remove em dash.

As per coding guidelines, avoid em dashes. Use periods or restructure the sentence instead.

✍️ Suggested revision
-A group with both `subgraphs` and `matching` matches a subgraph if it appears in either (the union — list-match OR regex-match). The group's rules apply at most once per subgraph regardless of how many parts of the selector match.
+A group with both `subgraphs` and `matching` matches a subgraph if it appears in either the explicit list or matches the regex. The group's rules apply at most once per subgraph regardless of how many parts of the selector match.
🤖 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
`@docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx`
at line 97, The sentence starting "A group with both `subgraphs` and `matching`
matches a subgraph if it appears in either (the union — list-match OR
regex-match)." uses an em dash; replace the em dash with a period or restructure
the clause to avoid punctuation rules violation. For example, split into two
sentences or use parentheses/commas so the line describing the union uses plain
punctuation: keep the original identifiers `subgraphs` and `matching` and ensure
the following sentence still communicates "list-match OR regex-match" and that
"The group's rules apply at most once per subgraph..." remains unchanged.

82-82: ⚡ Quick win

Remove hedging connector "so".

Replace the hedging connector with a direct statement. As per coding guidelines, avoid filler and hedging words.

✍️ Suggested revision
-Exact-name rules apply last, so a `headers.subgraphs.products` rule can still override a broader group rule when both target the same header.
+Exact-name rules apply last. A `headers.subgraphs.products` rule can override a broader group rule when both target the same header.
🤖 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
`@docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx`
at line 82, The sentence containing "headers.subgraphs.products" uses a hedging
connector "so"—replace it with a direct statement by removing "so" and
rephrasing to: "Exact-name rules apply last. A `headers.subgraphs.products` rule
can still override a broader group rule when both target the same header."
Update the line that currently reads "Exact-name rules apply last, so a
`headers.subgraphs.products` rule can still override..." to the two concise
sentences above.

91-91: ⚡ Quick win

Split long sentence.

The sentence has multiple clauses separated by a semicolon. As per coding guidelines, prefer short, declarative sentences and consider splitting sentences with more than one comma-separated clause.

✍️ Suggested revision
-* `negate_match` — If `true`, the regex result is inverted. Subgraphs in the explicit `subgraphs` list are still included positively; `negate_match` only affects the regex.
+* `negate_match` — If `true`, the regex result is inverted. Subgraphs in the explicit `subgraphs` list are still included positively. The negation only affects the regex.
🤖 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
`@docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx`
at line 91, The sentence describing `negate_match` is too long and should be
split into two short declarative sentences: one stating that `negate_match`
inverts the regex result when set to `true`, and a second clarifying that
subgraphs listed explicitly in `subgraphs` are always included and are not
affected by `negate_match`. Update the text around the `negate_match`
description to reflect these two separate sentences.
docs-website/router/proxy-capabilities/subgraph-response-header-operations.mdx (1)

84-85: ⚡ Quick win

Split long sentence and remove hedging connector.

The paragraph contains a long sentence with multiple clauses and uses "so" as a hedging connector. As per coding guidelines, prefer short, declarative sentences and avoid filler and hedging words.

✍️ Suggested revision
-The `groups` section applies a bundle of rules to every subgraph that matches the group's selector. A group can list subgraphs explicitly (`subgraphs:`), match by regex (`matching:`), or both. Groups apply after `all` and before exact `subgraphs` rules, so an exact subgraph rule can still override a group rule. See [Subgraph Request Headers Operations](/router/proxy-capabilities/subgraph-request-header-operations#subgraph-header-groups) for the full set of group fields and validation rules.
+The `groups` section applies a bundle of rules to every subgraph that matches the group's selector. A group can list subgraphs explicitly (`subgraphs:`), match by regex (`matching:`), or both. Groups apply after `all` and before exact `subgraphs` rules. An exact subgraph rule can override a group rule. See [Subgraph Request Headers Operations](/router/proxy-capabilities/subgraph-request-header-operations#subgraph-header-groups) for the full set of group fields and validation rules.
🤖 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
`@docs-website/router/proxy-capabilities/subgraph-response-header-operations.mdx`
around lines 84 - 85, The paragraph describing the groups behavior is one long
sentence and uses a hedging connector ("so"); split it into shorter declarative
sentences and remove the hedging word. Rephrase the sentence that currently
reads about application order and overrides into two clear sentences (e.g.,
state that "Groups apply after all and before exact subgraphs rules." then state
that "An exact subgraph rule can still override a group rule."), and preserve
the references to the group fields `subgraphs:`, `matching:`, and the sections
`all` and `subgraphs` so the meaning and validation guidance remain intact.
router/core/header_rule_engine_buildheader_test.go (1)

595-619: ⚡ Quick win

Strengthen the “fires only once” assertion.

This case currently uses a constant op: set, so it can still pass if the same group is applied twice. Add a direct dedup assertion on rule collection for the overlap case.

Proposed test hardening
  t.Run("hybrid match fires only once when both list and regex would match", func(t *testing.T) {
-   ht, err := NewHeaderPropagation(&config.HeaderRules{
+   rules := &config.HeaderRules{
      Groups: []*config.SubgraphHeaderGroup{
        {
          ID:        "double-match",
          Subgraphs: []string{"products"},
          Matching:  "^products$", // overlaps with list intentionally
          Request: []*config.RequestHeaderRule{
            {Operation: "set", Name: "X-Tag", Value: "once"},
          },
        },
      },
-   })
+   }
+
+   // Overlapping selectors must contribute the group exactly once.
+   gotRules := SubgraphRules(rules, "products")
+   require.Len(t, gotRules, 1)
+
+   ht, err := NewHeaderPropagation(rules)
    require.NoError(t, err)
    ctx := newGroupTestRequestContext(t, nil)
    hdr, _ := ht.BuildRequestHeaderForSubgraph("products", ctx)
    require.NotNil(t, hdr)
    assert.Equal(t, []string{"once"}, hdr.Values("X-Tag"))
  })
🤖 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 `@router/core/header_rule_engine_buildheader_test.go` around lines 595 - 619,
The test must assert not only that the resulting header value isn't duplicated
but that the propagation rule is present only once in the compiled rule
collection; after creating ht via NewHeaderPropagation(&config.HeaderRules{...})
inspect ht's compiled request-rule collection for the "products" subgraph and
count rules originating from the SubgraphHeaderGroup with ID "double-match" (or
inspect ht.Groups/ht.requestRulesForSubgraph("products") if that accessor
exists) and assert the count == 1, then proceed to call
BuildRequestHeaderForSubgraph("products", ctx) and keep the existing header
assertion to ensure deduplication both at rule-registration and runtime.
router/pkg/config/config.schema.json (1)

1905-1963: ⚡ Quick win

Enforce group selector/rule presence at schema level.

groups currently requires only id, so structurally invalid groups can pass schema validation and fail later at startup. Add anyOf constraints for selector and rule presence to fail fast.

Proposed schema tightening
         "groups": {
           "type": "array",
@@
           "items": {
             "type": "object",
             "additionalProperties": false,
             "required": ["id"],
+            "allOf": [
+              {
+                "anyOf": [
+                  { "required": ["subgraphs"] },
+                  { "required": ["matching"] }
+                ]
+              },
+              {
+                "anyOf": [
+                  { "required": ["request"] },
+                  { "required": ["response"] }
+                ]
+              }
+            ],
             "properties": {
🤖 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 `@router/pkg/config/config.schema.json` around lines 1905 - 1963, The group
object under "groups" currently only requires "id" which lets invalid group
entries pass validation; update the group's schema (the object whose properties
include "id", "subgraphs", "matching", "negate_match", "request", "response") to
add two anyOf constraints: one anyOf requiring at least one selector present
({"required":["subgraphs"]} or {"required":["matching"]}) and a second anyOf
requiring at least one rule array present ({"required":["request"]} or
{"required":["response"]}), keeping existing types/defaults intact so a group
must specify a selector and at least one of request/response.
router/core/header_rule_engine.go (1)

456-466: ⚡ Quick win

Skip group matching when no relevant group rules exist.

Line 460 still walks groups for response-only configs, and Line 642 does the same for request-only configs. On these hot paths that adds an avoidable allocation/scan even though no group can contribute headers. Track a hasAnyGroupResponseRules flag alongside hasAnyGroupRequestRules, then guard both calls before entering forEachMatchingGroup.

♻️ Proposed change
 type HeaderPropagation struct {
   ...
   hasAnyGroupRequestRules bool
+  hasAnyGroupResponseRules bool
   hasAnyGroupRegex bool
 }
 
 func (h *HeaderPropagation) indexGroups() error {
   ...
   if len(g.Request) > 0 {
     h.hasAnyGroupRequestRules = true
   }
+  if len(g.Response) > 0 {
+    h.hasAnyGroupResponseRules = true
+  }
 }
 
- if subgraphName != "" {
+ if subgraphName != "" && h.hasAnyGroupRequestRules {
   h.forEachMatchingGroup(subgraphName, func(g *config.SubgraphHeaderGroup) {
     for _, rule := range g.Request {
       h.applyRequestRuleToHeader(ctx, outHeader, rule)
     }
   })
 }
 
- if subgraphName != "" {
+ if subgraphName != "" && h.hasAnyGroupResponseRules {
   h.forEachMatchingGroup(subgraphName, func(g *config.SubgraphHeaderGroup) {
     for _, rule := range g.Response {
       h.applyResponseRule(propagation, resp, rule)
     }
   })
 }

Also applies to: 639-649

🤖 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 `@router/core/header_rule_engine.go` around lines 456 - 466, The group-matching
loop currently always calls h.forEachMatchingGroup even when no group can
contribute request or response headers; add two boolean flags
(hasAnyGroupRequestRules and hasAnyGroupResponseRules) that track whether any
SubgraphHeaderGroup contains Request or Response rules, then guard the calls
that iterate groups: only call h.forEachMatchingGroup for request rules when
hasAnyGroupRequestRules is true (the block that invokes
h.applyRequestRuleToHeader and references g.Request) and only call it for
response rules when hasAnyGroupResponseRules is true (the block that invokes
h.applyResponseRuleToHeader and references g.Response); compute/set these flags
where groups are loaded/configured so the hot path skips the
forEachMatchingGroup allocation/scan when irrelevant.
🤖 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.

Nitpick comments:
In
`@docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx`:
- Around line 72-73: Remove the promotional phrasing in the paragraph about
groups — specifically delete the sentence starting with "This is useful for
applying..." and replace it with a factual statement describing behavior only:
that a group applies the listed bundle of rules to every subgraph matching the
group's selector and can target subgraphs either via explicit subgraphs:
entries, via matching: regex patterns, or both (referencing "groups",
"subgraphs:", and "matching:"). Keep the explanation terse and factual without
value-laden words.
- Line 97: The sentence starting "A group with both `subgraphs` and `matching`
matches a subgraph if it appears in either (the union — list-match OR
regex-match)." uses an em dash; replace the em dash with a period or restructure
the clause to avoid punctuation rules violation. For example, split into two
sentences or use parentheses/commas so the line describing the union uses plain
punctuation: keep the original identifiers `subgraphs` and `matching` and ensure
the following sentence still communicates "list-match OR regex-match" and that
"The group's rules apply at most once per subgraph..." remains unchanged.
- Line 82: The sentence containing "headers.subgraphs.products" uses a hedging
connector "so"—replace it with a direct statement by removing "so" and
rephrasing to: "Exact-name rules apply last. A `headers.subgraphs.products` rule
can still override a broader group rule when both target the same header."
Update the line that currently reads "Exact-name rules apply last, so a
`headers.subgraphs.products` rule can still override..." to the two concise
sentences above.
- Line 91: The sentence describing `negate_match` is too long and should be
split into two short declarative sentences: one stating that `negate_match`
inverts the regex result when set to `true`, and a second clarifying that
subgraphs listed explicitly in `subgraphs` are always included and are not
affected by `negate_match`. Update the text around the `negate_match`
description to reflect these two separate sentences.

In
`@docs-website/router/proxy-capabilities/subgraph-response-header-operations.mdx`:
- Around line 84-85: The paragraph describing the groups behavior is one long
sentence and uses a hedging connector ("so"); split it into shorter declarative
sentences and remove the hedging word. Rephrase the sentence that currently
reads about application order and overrides into two clear sentences (e.g.,
state that "Groups apply after all and before exact subgraphs rules." then state
that "An exact subgraph rule can still override a group rule."), and preserve
the references to the group fields `subgraphs:`, `matching:`, and the sections
`all` and `subgraphs` so the meaning and validation guidance remain intact.

In `@router/core/header_rule_engine_buildheader_test.go`:
- Around line 595-619: The test must assert not only that the resulting header
value isn't duplicated but that the propagation rule is present only once in the
compiled rule collection; after creating ht via
NewHeaderPropagation(&config.HeaderRules{...}) inspect ht's compiled
request-rule collection for the "products" subgraph and count rules originating
from the SubgraphHeaderGroup with ID "double-match" (or inspect
ht.Groups/ht.requestRulesForSubgraph("products") if that accessor exists) and
assert the count == 1, then proceed to call
BuildRequestHeaderForSubgraph("products", ctx) and keep the existing header
assertion to ensure deduplication both at rule-registration and runtime.

In `@router/core/header_rule_engine.go`:
- Around line 456-466: The group-matching loop currently always calls
h.forEachMatchingGroup even when no group can contribute request or response
headers; add two boolean flags (hasAnyGroupRequestRules and
hasAnyGroupResponseRules) that track whether any SubgraphHeaderGroup contains
Request or Response rules, then guard the calls that iterate groups: only call
h.forEachMatchingGroup for request rules when hasAnyGroupRequestRules is true
(the block that invokes h.applyRequestRuleToHeader and references g.Request) and
only call it for response rules when hasAnyGroupResponseRules is true (the block
that invokes h.applyResponseRuleToHeader and references g.Response); compute/set
these flags where groups are loaded/configured so the hot path skips the
forEachMatchingGroup allocation/scan when irrelevant.

In `@router/pkg/config/config.schema.json`:
- Around line 1905-1963: The group object under "groups" currently only requires
"id" which lets invalid group entries pass validation; update the group's schema
(the object whose properties include "id", "subgraphs", "matching",
"negate_match", "request", "response") to add two anyOf constraints: one anyOf
requiring at least one selector present ({"required":["subgraphs"]} or
{"required":["matching"]}) and a second anyOf requiring at least one rule array
present ({"required":["request"]} or {"required":["response"]}), keeping
existing types/defaults intact so a group must specify a selector and at least
one of request/response.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 135b0afa-29af-4064-a911-0215d11d7f3e

📥 Commits

Reviewing files that changed from the base of the PR and between dfd3089 and 6088831.

📒 Files selected for processing (10)
  • docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx
  • docs-website/router/proxy-capabilities/subgraph-response-header-operations.mdx
  • router/core/header_rule_engine.go
  • router/core/header_rule_engine_buildheader_test.go
  • router/core/header_rule_engine_test.go
  • router/pkg/config/config.go
  • router/pkg/config/config.schema.json
  • router/pkg/config/fixtures/full.yaml
  • router/pkg/config/testdata/config_defaults.json
  • router/pkg/config/testdata/config_full.json

Adds explicit unit tests for the precedence behavior between rules
attached to the same subgraph through different layers and across
multiple groups. The router applies rules in a strict deterministic
order (headers.all -> headers.groups in config order -> exact
subgraphs.<name>) and the last writer wins per header name.

Tests cover:
- Two groups setting the same header — config order is the tiebreaker
- set vs propagate conflicts in both orders, with and without the
  client supplying the header
- propagate-without-default no-op behavior when client header missing
- propagate-with-default does overwrite an earlier set
- Last-writer-wins also applies within a single group's rule list
- Exact subgraph rules override every matching group regardless of
  group config order
- Rules touching different header names coexist regardless of order
- Swapping group order swaps the winner (the assertion in pure form)

Also adds a 'Conflict resolution' subsection to the request-header
docs explaining the precedence rules and the propagate/set asymmetry.
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.

🧹 Nitpick comments (2)
docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx (2)

97-97: ⚡ Quick win

Remove em dash.

The guideline states: "Avoid em dashes. Use periods or restructure the sentence instead."

Suggested rewrite
-A group with both `subgraphs` and `matching` matches a subgraph if it appears in either (the union — list-match OR regex-match). The group's rules apply at most once per subgraph regardless of how many parts of the selector match.
+A group with both `subgraphs` and `matching` matches a subgraph if it appears in either (union semantics: list-match OR regex-match). The group's rules apply at most once per subgraph regardless of how many parts of the selector match.

As per coding guidelines: Avoid em dashes. Use periods or restructure the sentence instead.

🤖 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
`@docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx`
at line 97, Replace the em dash in the sentence containing "`subgraphs` and
`matching`" so it follows the guideline (avoid em dashes); e.g., change "(the
union — list-match OR regex-match)" to "(the union: list-match OR regex-match)"
or "(the union (list-match OR regex-match))" and ensure the rest of the sentence
("The group's rules apply at most once per subgraph regardless of how many parts
of the selector match.") remains unchanged; update the text in the file where
the phrase '`subgraphs` and `matching`' appears.

109-109: ⚡ Quick win

Remove em dash.

The guideline states: "Avoid em dashes. Use periods or restructure the sentence instead."

Suggested rewrite
-Concretely, **within the groups layer, group order in the YAML is the tiebreaker** — entries are merged first-to-last, and a later group's rule overrides an earlier group's rule for the same header name. The `headers.subgraphs.<name>` layer always runs last and overrides every group rule for that exact subgraph.
+Concretely, **within the groups layer, group order in the YAML is the tiebreaker**. Entries are merged first-to-last, and a later group's rule overrides an earlier group's rule for the same header name. The `headers.subgraphs.<name>` layer always runs last and overrides every group rule for that exact subgraph.

As per coding guidelines: Avoid em dashes. Use periods or restructure the sentence instead.

🤖 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
`@docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx`
at line 109, The sentence "Concretely, **within the groups layer, group order in
the YAML is the tiebreaker** — entries are merged first-to-last, and a later
group's rule overrides an earlier group's rule for the same header name." uses
an em dash; remove it and split/restructure into two sentences (or use a period)
so it reads e.g. "Concretely, within the groups layer, group order in the YAML
is the tiebreaker. Entries are merged first-to-last, and a later group's rule
overrides an earlier group's rule for the same header name. The
`headers.subgraphs.<name>` layer always runs last and overrides every group rule
for that exact subgraph." Apply this change to the paragraph referencing the
groups layer and the `headers.subgraphs.<name>` layer.
🤖 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.

Nitpick comments:
In
`@docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx`:
- Line 97: Replace the em dash in the sentence containing "`subgraphs` and
`matching`" so it follows the guideline (avoid em dashes); e.g., change "(the
union — list-match OR regex-match)" to "(the union: list-match OR regex-match)"
or "(the union (list-match OR regex-match))" and ensure the rest of the sentence
("The group's rules apply at most once per subgraph regardless of how many parts
of the selector match.") remains unchanged; update the text in the file where
the phrase '`subgraphs` and `matching`' appears.
- Line 109: The sentence "Concretely, **within the groups layer, group order in
the YAML is the tiebreaker** — entries are merged first-to-last, and a later
group's rule overrides an earlier group's rule for the same header name." uses
an em dash; remove it and split/restructure into two sentences (or use a period)
so it reads e.g. "Concretely, within the groups layer, group order in the YAML
is the tiebreaker. Entries are merged first-to-last, and a later group's rule
overrides an earlier group's rule for the same header name. The
`headers.subgraphs.<name>` layer always runs last and overrides every group rule
for that exact subgraph." Apply this change to the paragraph referencing the
groups layer and the `headers.subgraphs.<name>` layer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8ac316f6-b6d0-4dcf-844e-6c1ea5132f64

📥 Commits

Reviewing files that changed from the base of the PR and between 6088831 and 855faeb.

📒 Files selected for processing (2)
  • docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx
  • router/core/header_rule_engine_buildheader_test.go

@mwisner mwisner marked this pull request as ready for review May 21, 2026 10:44
@mwisner mwisner requested review from a team as code owners May 21, 2026 10:44
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 pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

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.

1 participant