diff --git a/docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx b/docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx index 005e33b389..c48fccd2d6 100644 --- a/docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx +++ b/docs-website/router/proxy-capabilities/subgraph-request-header-operations.mdx @@ -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? @@ -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.` 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. + +Subgraph names are case-sensitive identifiers. Invalid regular expressions fail router startup. + ### Supported header rules Currently, we support the following header rules: diff --git a/docs-website/router/proxy-capabilities/subgraph-response-header-operations.mdx b/docs-website/router/proxy-capabilities/subgraph-response-header-operations.mdx index c2ad93ebfe..f23936ac49 100644 --- a/docs-website/router/proxy-capabilities/subgraph-response-header-operations.mdx +++ b/docs-website/router/proxy-capabilities/subgraph-response-header-operations.mdx @@ -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? @@ -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: diff --git a/router/core/header_rule_engine.go b/router/core/header_rule_engine.go index bcc6f75438..bdc8a6a1a2 100644 --- a/router/core/header_rule_engine.go +++ b/router/core/header_rule_engine.go @@ -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) { @@ -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 @@ -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 } @@ -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 { @@ -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 @@ -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. @@ -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 { @@ -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 @@ -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...) diff --git a/router/core/header_rule_engine_buildheader_test.go b/router/core/header_rule_engine_buildheader_test.go index c652deeed4..05e5e326c0 100644 --- a/router/core/header_rule_engine_buildheader_test.go +++ b/router/core/header_rule_engine_buildheader_test.go @@ -443,3 +443,235 @@ func TestSubgraphHeadersBuilder_ConcurrentAccessSameSubgraph(t *testing.T) { } } } + +// TestBuildRequestHeaderForSubgraph_PatternRules exercises the subgraph_patterns +// selector. Patterns let users target a base subgraph and its feature subgraphs +// (e.g. PR-preview deployments named like "products-feature-pr-123") without +// duplicating the same per-subgraph block for every variant. +func TestBuildRequestHeaderForSubgraph_PatternRules(t *testing.T) { + t.Run("regex selector matches base and feature subgraphs", func(t *testing.T) { + ht, err := NewHeaderPropagation(&config.HeaderRules{ + SubgraphPatterns: []*config.SubgraphPatternHeaderRule{ + { + Matching: "^products(-feature-.+)?$", + Request: []*config.RequestHeaderRule{ + {Operation: "propagate", Named: "X-Products-Auth"}, + }, + }, + }, + }) + require.NoError(t, err) + + clientReq := httptest.NewRequest("POST", "http://localhost", nil) + clientReq.Header.Set("X-Products-Auth", "secret") + + ctx := &requestContext{ + logger: zap.NewNop(), + responseWriter: httptest.NewRecorder(), + request: clientReq, + operation: &operationContext{}, + subgraphResolver: NewSubgraphResolver(nil), + } + + base, _ := ht.BuildRequestHeaderForSubgraph("products", ctx) + require.NotNil(t, base) + assert.Equal(t, "secret", base.Get("X-Products-Auth")) + + feat, _ := ht.BuildRequestHeaderForSubgraph("products-feature-pr-123", ctx) + require.NotNil(t, feat) + assert.Equal(t, "secret", feat.Get("X-Products-Auth")) + + // Unrelated subgraph should not pick up the rule. + other, otherHash := ht.BuildRequestHeaderForSubgraph("inventory", ctx) + assert.Nil(t, other) + assert.Equal(t, uint64(0), otherHash) + }) + + t.Run("pattern with negate_match applies to non-matching names", func(t *testing.T) { + ht, err := NewHeaderPropagation(&config.HeaderRules{ + SubgraphPatterns: []*config.SubgraphPatternHeaderRule{ + { + Matching: "^internal-.+$", + NegateMatch: true, + Request: []*config.RequestHeaderRule{ + {Operation: "propagate", Named: "X-Public"}, + }, + }, + }, + }) + require.NoError(t, err) + + clientReq := httptest.NewRequest("POST", "http://localhost", nil) + clientReq.Header.Set("X-Public", "yes") + + ctx := &requestContext{ + logger: zap.NewNop(), + responseWriter: httptest.NewRecorder(), + request: clientReq, + operation: &operationContext{}, + subgraphResolver: NewSubgraphResolver(nil), + } + + external, _ := ht.BuildRequestHeaderForSubgraph("products", ctx) + require.NotNil(t, external) + assert.Equal(t, "yes", external.Get("X-Public")) + + internal, hash := ht.BuildRequestHeaderForSubgraph("internal-billing", ctx) + assert.Nil(t, internal) + assert.Equal(t, uint64(0), hash) + }) + + t.Run("invalid pattern regex fails NewHeaderPropagation", func(t *testing.T) { + _, err := NewHeaderPropagation(&config.HeaderRules{ + SubgraphPatterns: []*config.SubgraphPatternHeaderRule{ + {Matching: "[invalid"}, + }, + }) + require.Error(t, err) + }) + + t.Run("missing matching value fails NewHeaderPropagation", func(t *testing.T) { + _, err := NewHeaderPropagation(&config.HeaderRules{ + SubgraphPatterns: []*config.SubgraphPatternHeaderRule{ + { + Request: []*config.RequestHeaderRule{ + {Operation: "propagate", Named: "X-A"}, + }, + }, + }, + }) + require.Error(t, err) + }) + + t.Run("rule order: all -> patterns -> exact", func(t *testing.T) { + // Each layer sets the same header to a different value. The exact rule must + // win because exact subgraph rules are applied last and `set` overwrites. + ht, err := NewHeaderPropagation(&config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Request: []*config.RequestHeaderRule{ + {Operation: "set", Name: "X-Layer", Value: "all"}, + }, + }, + SubgraphPatterns: []*config.SubgraphPatternHeaderRule{ + { + Matching: "^products.*$", + Request: []*config.RequestHeaderRule{ + {Operation: "set", Name: "X-Layer", Value: "pattern"}, + }, + }, + }, + Subgraphs: map[string]*config.GlobalHeaderRule{ + "products": { + Request: []*config.RequestHeaderRule{ + {Operation: "set", Name: "X-Layer", Value: "exact"}, + }, + }, + }, + }) + require.NoError(t, err) + + clientReq := httptest.NewRequest("POST", "http://localhost", nil) + ctx := &requestContext{ + logger: zap.NewNop(), + responseWriter: httptest.NewRecorder(), + request: clientReq, + operation: &operationContext{}, + subgraphResolver: NewSubgraphResolver(nil), + } + + // Exact match: all three layers run; exact wins. + exact, _ := ht.BuildRequestHeaderForSubgraph("products", ctx) + require.NotNil(t, exact) + assert.Equal(t, "exact", exact.Get("X-Layer")) + + // Feature subgraph matches the pattern but has no exact entry; pattern wins. + feat, _ := ht.BuildRequestHeaderForSubgraph("products-feature-pr-123", ctx) + require.NotNil(t, feat) + assert.Equal(t, "pattern", feat.Get("X-Layer")) + + // Unrelated subgraph: only `all` runs. + other, _ := ht.BuildRequestHeaderForSubgraph("inventory", ctx) + require.NotNil(t, other) + assert.Equal(t, "all", other.Get("X-Layer")) + }) + + t.Run("pattern alone causes hasRequestRulesForSubgraph to be true for matched names", func(t *testing.T) { + // Without `all` rules and without exact entries, a pattern match alone + // must be enough for the builder to produce a header set. + ht, err := NewHeaderPropagation(&config.HeaderRules{ + SubgraphPatterns: []*config.SubgraphPatternHeaderRule{ + { + Matching: "^matches-me-.+$", + Request: []*config.RequestHeaderRule{ + {Operation: "propagate", Named: "X-Pattern"}, + }, + }, + }, + }) + require.NoError(t, err) + + clientReq := httptest.NewRequest("POST", "http://localhost", nil) + clientReq.Header.Set("X-Pattern", "v") + + ctx := &requestContext{ + logger: zap.NewNop(), + responseWriter: httptest.NewRecorder(), + request: clientReq, + operation: &operationContext{}, + subgraphResolver: NewSubgraphResolver(nil), + } + + matched, hashMatched := ht.BuildRequestHeaderForSubgraph("matches-me-1", ctx) + require.NotNil(t, matched) + require.NotZero(t, hashMatched) + assert.Equal(t, "v", matched.Get("X-Pattern")) + + unmatched, hashUnmatched := ht.BuildRequestHeaderForSubgraph("nope", ctx) + assert.Nil(t, unmatched) + assert.Equal(t, uint64(0), hashUnmatched) + }) +} + +// TestSubgraphRules_PatternsIncluded confirms the helper used by the engine +// to compute the static set of "potentially propagated" header names also +// surfaces rules contributed by subgraph_patterns. Without this, the engine's +// single-flight key would not include pattern-propagated headers. +func TestSubgraphRules_PatternsIncluded(t *testing.T) { + rules := &config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Request: []*config.RequestHeaderRule{ + {Operation: "propagate", Named: "X-All"}, + }, + }, + SubgraphPatterns: []*config.SubgraphPatternHeaderRule{ + { + Matching: "^products(-feature-.+)?$", + Request: []*config.RequestHeaderRule{ + {Operation: "propagate", Named: "X-Pattern"}, + }, + }, + }, + Subgraphs: map[string]*config.GlobalHeaderRule{ + "products": { + Request: []*config.RequestHeaderRule{ + {Operation: "propagate", Named: "X-Exact"}, + }, + }, + }, + } + + got := SubgraphRules(rules, "products") + require.Len(t, got, 3) + assert.Equal(t, "X-All", got[0].Named) + assert.Equal(t, "X-Pattern", got[1].Named) + assert.Equal(t, "X-Exact", got[2].Named) + + gotFeature := SubgraphRules(rules, "products-feature-pr-123") + require.Len(t, gotFeature, 2) + assert.Equal(t, "X-All", gotFeature[0].Named) + assert.Equal(t, "X-Pattern", gotFeature[1].Named) + + gotOther := SubgraphRules(rules, "inventory") + require.Len(t, gotOther, 1) + assert.Equal(t, "X-All", gotOther[0].Named) +} diff --git a/router/core/header_rule_engine_test.go b/router/core/header_rule_engine_test.go index 6a8f2711f5..c033d5860f 100644 --- a/router/core/header_rule_engine_test.go +++ b/router/core/header_rule_engine_test.go @@ -1,6 +1,7 @@ package core import ( + "context" "net/http" "sync" "testing" @@ -384,3 +385,50 @@ func TestNewHeaderPropagation(t *testing.T) { assert.False(t, hp.HasResponseRules()) }) } + +// TestApplyResponseHeaderRules_PatternRules verifies that subgraph_patterns +// also drive response-side header propagation. A feature subgraph whose name +// matches the configured selector must have its response headers propagated +// to the client just as the matching base subgraph would. +func TestApplyResponseHeaderRules_PatternRules(t *testing.T) { + t.Parallel() + + hp, err := NewHeaderPropagation(&config.HeaderRules{ + SubgraphPatterns: []*config.SubgraphPatternHeaderRule{ + { + Matching: "^products(-feature-.+)?$", + Response: []*config.ResponseHeaderRule{ + { + Operation: config.HeaderRuleOperationPropagate, + Named: "X-Products-Trace", + Algorithm: config.ResponseHeaderRuleAlgorithmLastWrite, + }, + }, + }, + }, + }) + require.NoError(t, err) + + prop := &responseHeaderPropagation{ + header: make(http.Header), + m: &sync.Mutex{}, + } + ctx := context.WithValue(context.Background(), responseHeaderPropagationKey{}, prop) + + subgraphResp := http.Header{} + subgraphResp.Set("X-Products-Trace", "abc-123") + + hp.ApplyResponseHeaderRules(ctx, subgraphResp, "products-feature-pr-7", 200, nil) + assert.Equal(t, "abc-123", prop.header.Get("X-Products-Trace")) + + // Ensure unrelated subgraphs are not affected. + prop2 := &responseHeaderPropagation{ + header: make(http.Header), + m: &sync.Mutex{}, + } + ctx2 := context.WithValue(context.Background(), responseHeaderPropagationKey{}, prop2) + otherResp := http.Header{} + otherResp.Set("X-Products-Trace", "should-not-propagate") + hp.ApplyResponseHeaderRules(ctx2, otherResp, "inventory", 200, nil) + assert.Equal(t, "", prop2.header.Get("X-Products-Trace")) +} diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index 8646eae8f6..057e3a3a59 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -280,10 +280,34 @@ type CacheControlPolicy struct { type HeaderRules struct { // All is a set of rules that apply to all requests - All *GlobalHeaderRule `yaml:"all,omitempty"` - Subgraphs map[string]*GlobalHeaderRule `yaml:"subgraphs,omitempty"` - CookieWhitelist []string `yaml:"cookie_whitelist,omitempty"` - Router RouterHeaderRules `yaml:"router,omitempty"` + All *GlobalHeaderRule `yaml:"all,omitempty"` + // Subgraphs is a map of rules keyed by exact subgraph name. The name must match the + // subgraph name in the Studio. + Subgraphs map[string]*GlobalHeaderRule `yaml:"subgraphs,omitempty"` + // SubgraphPatterns is an ordered list of rules whose `matching` selector is a Go + // regular expression evaluated against the subgraph name. This allows targeting a + // group of subgraphs (e.g. a base subgraph and its feature subgraphs) without + // duplicating the same rule under every individual subgraph entry. Patterns are + // applied in the order they are defined, after `all` and before exact `subgraphs` + // matches, so an exact subgraph rule can still override a pattern rule. + SubgraphPatterns []*SubgraphPatternHeaderRule `yaml:"subgraph_patterns,omitempty"` + CookieWhitelist []string `yaml:"cookie_whitelist,omitempty"` + Router RouterHeaderRules `yaml:"router,omitempty"` +} + +// SubgraphPatternHeaderRule applies a set of request/response header rules to every +// subgraph whose name matches the `Matching` regex selector. Selectors are compiled at +// startup; an invalid pattern will fail router initialization. +type SubgraphPatternHeaderRule struct { + // Matching is a Go regular expression evaluated against the subgraph name. + // The match is case-sensitive (subgraph names are case-sensitive identifiers). + Matching string `yaml:"matching"` + // NegateMatch inverts the regex result, useful for "all subgraphs except X" rules. + NegateMatch bool `yaml:"negate_match,omitempty"` + // Request rules to apply to every subgraph matched by this pattern. + Request []*RequestHeaderRule `yaml:"request,omitempty"` + // Response rules to apply to every subgraph matched by this pattern. + Response []*ResponseHeaderRule `yaml:"response,omitempty"` } type RouterHeaderRules struct { diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index d300d76a80..e042fe9521 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -1902,6 +1902,52 @@ } } }, + "subgraph_patterns": { + "type": "array", + "description": "An ordered list of header rules that apply to every subgraph whose name matches the regular expression in 'matching'. Patterns are evaluated after 'all' and before exact 'subgraphs' rules, so an exact subgraph rule can still override a pattern rule. This is useful for targeting a base subgraph and its feature subgraphs with the same forwarding rules without duplicating the configuration.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["matching"], + "properties": { + "matching": { + "type": "string", + "description": "A Go regular expression evaluated against the subgraph name. Subgraph names are case-sensitive." + }, + "negate_match": { + "type": "boolean", + "description": "If true, the regex result is inverted, applying the rule to every subgraph whose name does NOT match.", + "default": false + }, + "request": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/traffic_shaping_header_rule" + }, + { + "$ref": "#/$defs/set_header_rule" + } + ] + } + }, + "response": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/traffic_shaping_header_response_rule" + }, + { + "$ref": "#/$defs/set_header_rule" + } + ] + } + } + } + } + }, "cookie_whitelist": { "type": "array", "description": "A list of Cookie keys allowed to be forwarded to the subgraph. If the list is empty or unspecified, all cookies are forwarded. This option will do nothing if the 'Cookie' header is not propagated.", diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index 5c7bbc25da..b67adc9f12 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -198,6 +198,7 @@ "Headers": { "All": null, "Subgraphs": null, + "SubgraphPatterns": null, "CookieWhitelist": null, "Router": { "Response": null diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index f084bf9f28..74d89a1fa8 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -370,6 +370,7 @@ ] } }, + "SubgraphPatterns": null, "CookieWhitelist": [ "cookie1", "cookie2"