Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ headers:
- op: "propagate"
named: "Subgraph-Secret"
default: "some-secret"

subgraph_patterns: # Apply rules to every subgraph whose name matches the regex
- matching: "^products(-feature-.+)?$"
request:
- op: "propagate"
named: "X-Products-Auth"
```

### What does the snippet do?
Expand All @@ -61,6 +67,29 @@ With `all` we address all subgraph requests. Next, we can define several rules o

The `subgraphs` section allows to propagate headers for specific subgraphs. The name must match with the subgraph name in the Studio.

The `subgraph_patterns` section applies rules to every subgraph whose name matches the regular expression in `matching`. This is useful for targeting a base subgraph and its feature subgraphs (for example, PR-preview deployments named `products-feature-pr-123`) without duplicating the same block under every variant.

### Rule evaluation order

Rules are applied in this order:

1. `headers.all` rules.
2. `headers.subgraph_patterns` entries whose `matching` regex matches the subgraph name, in config order.
3. `headers.subgraphs.<name>` rules for the exact subgraph name.

Because exact rules are applied last, a rule under `headers.subgraphs.products` can still override a broader pattern rule when both target the same header.

### Pattern selectors

Each entry under `subgraph_patterns` accepts:

* `matching` — A Go regular expression evaluated against the subgraph name. Required.
* `negate_match` — If `true`, the regex result is inverted. Useful for "all subgraphs except X" rules.
* `request` — Request rules to apply when the pattern matches.
* `response` — Response rules to apply when the pattern matches.
Comment on lines +86 to +89
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace em dashes in selector bullets.

The bullets use em dashes, which conflicts with the docs style rule for MDX files.

Suggested edit
-* `matching` — A Go regular expression evaluated against the subgraph name. Required.
-* `negate_match` — If `true`, the regex result is inverted. Useful for "all subgraphs except X" rules.
-* `request` — Request rules to apply when the pattern matches.
-* `response` — Response rules to apply when the pattern matches.
+* `matching`: A Go regular expression evaluated against the subgraph name. Required.
+* `negate_match`: If `true`, the regex result is inverted. Useful for "all subgraphs except X" rules.
+* `request`: Request rules to apply when the pattern matches.
+* `response`: Response rules to apply when the pattern matches.

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

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* `matching` A Go regular expression evaluated against the subgraph name. Required.
* `negate_match` If `true`, the regex result is inverted. Useful for "all subgraphs except X" rules.
* `request` Request rules to apply when the pattern matches.
* `response` Response rules to apply when the pattern matches.
* `matching`: A Go regular expression evaluated against the subgraph name. Required.
* `negate_match`: If `true`, the regex result is inverted. Useful for "all subgraphs except X" rules.
* `request`: Request rules to apply when the pattern matches.
* `response`: Response rules to apply when the pattern matches.
🤖 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 86 - 89, The list items for the selectors (`matching`,
`negate_match`, `request`, `response`) use em dashes; update each bullet in
subgraph-request-header-operations.mdx to remove the em dash and follow the MDX
docs style (replace the em dash with a period or rephrase into a short
sentence), e.g. "`matching` — A Go regular expression..." → "`matching`. A Go
regular expression..." ensuring punctuation and capitalization remain consistent
across all four entries.


Subgraph names are case-sensitive identifiers. Invalid regular expressions fail router startup.

### Supported header rules

Currently, we support the following header rules:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ headers:
named: "X-User-Id"
default: "456" # Set the value when the header was not set
algorithm: "last_write"

subgraph_patterns: # Apply rules to every subgraph whose name matches the regex
- matching: "^products(-feature-.+)?$"
response:
- op: "propagate"
named: "X-Products-Trace"
algorithm: "last_write"
```

### What does the snippet do?
Expand All @@ -72,6 +79,8 @@ With `all` we address all subgraph requests. Next, we can define several rules o

The `subgraphs` section allows to propagate headers for specific subgraphs. The name must match with the subgraph name in the Studio.

The `subgraph_patterns` section applies rules to every subgraph whose name matches the regular expression in `matching`. This is useful for targeting a base subgraph and its feature subgraphs without duplicating the same block under every variant. Patterns are evaluated after `all` and before exact `subgraphs` rules, so an exact subgraph rule can still override a pattern rule.

### Supported header rules

Currently, we support the following header rules:
Expand Down
139 changes: 136 additions & 3 deletions router/core/header_rule_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ type HeaderPropagation struct {
// Precomputed request rule presence for fast-path checks
hasAllRequestRules bool
subgraphHasRequestRules map[string]bool
// subgraphPatternRegex holds the compiled regex for each subgraph_patterns rule.
// The slice is index-aligned with rules.SubgraphPatterns so we never have to
// recompile while serving traffic.
subgraphPatternRegex []*regexp.Regexp
// hasAnyPatternRequestRules is true when at least one subgraph_patterns entry
// defines request rules. Used as a fast-path so subgraphs that have no
// `all`/exact rules can still skip the entire builder when no patterns apply.
hasAnyPatternRequestRules bool
}

func initHeaderRules(rules *config.HeaderRules) {
Expand All @@ -179,6 +187,28 @@ func NewHeaderPropagation(rules *config.HeaderRules) (*HeaderPropagation, error)
compiledRouterResponseRules: map[string]*vm.Program{},
}

// Compile subgraph_patterns selectors up-front. Invalid patterns fail startup
// rather than only manifesting when a matching subgraph is composed.
if len(rules.SubgraphPatterns) > 0 {
hf.subgraphPatternRegex = make([]*regexp.Regexp, len(rules.SubgraphPatterns))
for i, p := range rules.SubgraphPatterns {
if p == nil {
return nil, fmt.Errorf("subgraph_patterns[%d] is nil", i)
}
if p.Matching == "" {
return nil, fmt.Errorf("subgraph_patterns[%d] is missing required field 'matching'", i)
}
re, err := regexp.Compile(p.Matching)
if err != nil {
return nil, fmt.Errorf("invalid regex %q for subgraph_patterns[%d]: %w", p.Matching, i, err)
}
hf.subgraphPatternRegex[i] = re
if len(p.Request) > 0 {
hf.hasAnyPatternRequestRules = true
}
}
}

rhrs, rhrrs, rrs := hf.getAllRules()
hf.hasRequestRules = len(rhrs) > 0
hf.hasResponseRules = len(rhrrs) > 0
Expand Down Expand Up @@ -246,11 +276,23 @@ func (h *HeaderPropagation) getAllRules() ([]*config.RequestHeaderRule, []*confi
for _, subgraph := range h.rules.Subgraphs {
rhrs = append(rhrs, subgraph.Request...)
}
for _, pattern := range h.rules.SubgraphPatterns {
if pattern == nil {
continue
}
rhrs = append(rhrs, pattern.Request...)
}

rhrrs := h.rules.All.Response
for _, subgraph := range h.rules.Subgraphs {
rhrrs = append(rhrrs, subgraph.Response...)
}
for _, pattern := range h.rules.SubgraphPatterns {
if pattern == nil {
continue
}
rhrrs = append(rhrrs, pattern.Response...)
}

return rhrs, rhrrs, h.rules.Router.Response
}
Expand Down Expand Up @@ -355,6 +397,23 @@ func (h *HeaderPropagation) BuildRequestHeaderForSubgraph(subgraphName string, c
h.applyRequestRuleToHeader(ctx, outHeader, rule)
}

// Apply pattern-based rules in config order. Patterns are evaluated after
// global rules but before exact subgraph rules so that an explicit per-subgraph
// rule can still override a broader pattern rule (e.g. via op: set).
if subgraphName != "" {
for i, pattern := range h.rules.SubgraphPatterns {
if pattern == nil || len(pattern.Request) == 0 {
continue
}
if !h.subgraphPatternMatches(i, subgraphName) {
continue
}
for _, rule := range pattern.Request {
h.applyRequestRuleToHeader(ctx, outHeader, rule)
}
}
}

// Apply subgraph-specific rules
if subgraphName != "" {
if subRules, ok := h.rules.Subgraphs[subgraphName]; ok {
Expand All @@ -368,8 +427,28 @@ func (h *HeaderPropagation) BuildRequestHeaderForSubgraph(subgraphName string, c
return outHeader, headerHash
}

// subgraphPatternMatches reports whether the subgraph_patterns rule at the given
// index matches the supplied subgraph name. It honors the rule's NegateMatch
// flag and assumes regexes were compiled successfully at startup.
func (h *HeaderPropagation) subgraphPatternMatches(index int, subgraphName string) bool {
if h == nil || index < 0 || index >= len(h.subgraphPatternRegex) {
return false
}
re := h.subgraphPatternRegex[index]
if re == nil {
return false
}
matched := re.MatchString(subgraphName)
if h.rules.SubgraphPatterns[index].NegateMatch {
matched = !matched
}
return matched
}

// hasRequestRulesForSubgraph returns true if there are request header rules
// that would apply to the given subgraph. The result is computed at creation time.
// that would apply to the given subgraph. The result is computed at creation time
// for `all` and exact-name rules, and computed on-demand for pattern rules
// because they depend on the runtime subgraph name.
func (h *HeaderPropagation) hasRequestRulesForSubgraph(subgraphName string) bool {
if h == nil || h.rules == nil {
return false
Expand All @@ -382,7 +461,20 @@ func (h *HeaderPropagation) hasRequestRulesForSubgraph(subgraphName string) bool
// No subgraph specified and no global rules
return false
}
return h.subgraphHasRequestRules != nil && h.subgraphHasRequestRules[subgraphName]
if h.subgraphHasRequestRules != nil && h.subgraphHasRequestRules[subgraphName] {
return true
}
if h.hasAnyPatternRequestRules {
for i, pattern := range h.rules.SubgraphPatterns {
if pattern == nil || len(pattern.Request) == 0 {
continue
}
if h.subgraphPatternMatches(i, subgraphName) {
return true
}
}
}
return false
}

// hashHeaderStable computes a deterministic 64-bit hash over the provided header map.
Expand Down Expand Up @@ -438,6 +530,23 @@ func (h *HeaderPropagation) ApplyResponseHeaderRules(ctx context.Context, header
h.applyResponseRule(propagation, resp, rule)
}

// Apply pattern-based rules in config order. Patterns are evaluated after
// global rules but before exact subgraph rules so an explicit per-subgraph
// rule can still override a broader pattern rule.
if subgraphName != "" {
for i, pattern := range h.rules.SubgraphPatterns {
if pattern == nil || len(pattern.Response) == 0 {
continue
}
if !h.subgraphPatternMatches(i, subgraphName) {
continue
}
for _, rule := range pattern.Response {
h.applyResponseRule(propagation, resp, rule)
}
}
}

if subgraphName != "" {
if subgraphRules, ok := h.rules.Subgraphs[subgraphName]; ok {
for _, rule := range subgraphRules.Response {
Expand Down Expand Up @@ -866,7 +975,11 @@ func createMostRestrictivePolicy(policies []*cachedirective.Object) (*cachedirec
return &result, cacheControlHeader
}

// SubgraphRules returns the list of header rules for the subgraph with the given name
// SubgraphRules returns the list of header rules for the subgraph with the given name.
// Rules are returned in evaluation order: `all` first, then any subgraph_patterns whose
// regex matches the subgraph name (in config order), then exact-name rules. Patterns
// with invalid regexes are skipped here; startup-time validation in NewHeaderPropagation
// is the source of truth for failing the router on bad configuration.
func SubgraphRules(rules *config.HeaderRules, subgraphName string) []*config.RequestHeaderRule {
if rules == nil {
return nil
Expand All @@ -875,6 +988,26 @@ func SubgraphRules(rules *config.HeaderRules, subgraphName string) []*config.Req
if rules.All != nil {
subgraphRules = append(subgraphRules, rules.All.Request...)
}
if subgraphName != "" && len(rules.SubgraphPatterns) > 0 {
for _, pattern := range rules.SubgraphPatterns {
if pattern == nil || len(pattern.Request) == 0 || pattern.Matching == "" {
continue
}
re, err := regexp.Compile(pattern.Matching)
if err != nil {
// Selectors are validated at startup; if compilation fails here we
// silently skip the rule rather than panicking on a request path.
continue
}
matched := re.MatchString(subgraphName)
if pattern.NegateMatch {
matched = !matched
}
if matched {
subgraphRules = append(subgraphRules, pattern.Request...)
}
}
}
if rules.Subgraphs != nil {
if subgraphSpecificRules, ok := rules.Subgraphs[subgraphName]; ok {
subgraphRules = append(subgraphRules, subgraphSpecificRules.Request...)
Expand Down
Loading
Loading