Skip to content

Commit

Permalink
feat(router): make header matching rules case insensitive (#1584)
Browse files Browse the repository at this point in the history
  • Loading branch information
df-wg authored Feb 13, 2025
1 parent b81b828 commit c0bceaa
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 18 deletions.
23 changes: 16 additions & 7 deletions router-tests/headers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ func TestForwardHeaders(t *testing.T) {

const (
// Make sure you copy these to the struct tag in the subscription test
headerNameInGlobalRule = "foo"
headerNameInSubgraphRule = "barista" // This matches the regex in test1 subgraph forwarding rules
headerValue = "bar"
headerValue2 = "baz"

subscriptionForGlobalRulePayload = `{"query": "subscription { headerValue(name:\"foo\", repeat:3) { value }}"}`
subscriptionForSubgraphRulePayload = `{"query": "subscription { headerValue(name:\"barista\", repeat:3) { value }}"}`
headerNameInGlobalRule = "foo"
headerNameInSubgraphRule = "barista" // This matches the regex in test1 subgraph forwarding rules
headerNameCaseInsensitiveRule = "bAz-CAse-Insensitive" // This matches the regex in test1 subgraph forwarding rules
headerValue = "bar"
headerValue2 = "baz"

subscriptionForGlobalRulePayload = `{"query": "subscription { headerValue(name:\"foo\", repeat:3) { value }}"}`
subscriptionForSubgraphRulePayload = `{"query": "subscription { headerValue(name:\"barista\", repeat:3) { value }}"}`
subscriptionForSubgraphCaseRulePayload = `{"query": "subscription { headerValue(name:\"baz-case-insensitive\", repeat:3) { value }}"}`
)

headerRules := config.HeaderRules{
Expand All @@ -42,6 +44,10 @@ func TestForwardHeaders(t *testing.T) {
Operation: config.HeaderRuleOperationPropagate,
Matching: "(?i)^bar.*",
},
{
Operation: config.HeaderRuleOperationPropagate,
Matching: "^baz-case-.*",
},
},
},
},
Expand All @@ -56,6 +62,7 @@ func TestForwardHeaders(t *testing.T) {
}{
{headerNameInGlobalRule, "global rule"},
{headerNameInSubgraphRule, "subgraph rule"},
{headerNameCaseInsensitiveRule, "subgraph rule"},
}
testenv.Run(t, &testenv.Config{
ModifyEngineExecutionConfiguration: func(cfg *config.EngineExecutionConfiguration) {
Expand Down Expand Up @@ -160,6 +167,7 @@ func TestForwardHeaders(t *testing.T) {
testName string
}{
{headerNameInSubgraphRule, subscriptionForSubgraphRulePayload, "subgraph rule"},
{headerNameCaseInsensitiveRule, subscriptionForSubgraphCaseRulePayload, "subgraph case insensitive rule"},
}
testenv.Run(t, &testenv.Config{
ModifyEngineExecutionConfiguration: func(cfg *config.EngineExecutionConfiguration) {
Expand Down Expand Up @@ -227,6 +235,7 @@ func TestForwardHeaders(t *testing.T) {
}{
{headerNameInGlobalRule, subscriptionForGlobalRulePayload, "global rule"},
{headerNameInSubgraphRule, subscriptionForSubgraphRulePayload, "subgraph rule"},
{headerNameCaseInsensitiveRule, subscriptionForSubgraphCaseRulePayload, "subgraph case insensitive rule"},
}
testenv.Run(t, &testenv.Config{
ModifyEngineExecutionConfiguration: func(cfg *config.EngineExecutionConfiguration) {
Expand Down
12 changes: 7 additions & 5 deletions router/core/header_rule_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ var (
"Sec-Websocket-Protocol",
"Sec-Websocket-Version",
}
cacheControlKey = "Cache-Control"
expiresKey = "Expires"
noCache = "no-cache"
cacheControlKey = "Cache-Control"
expiresKey = "Expires"
noCache = "no-cache"
caseInsensitiveRegexp = "(?i)"
)

type responseHeaderPropagationKey struct{}
Expand Down Expand Up @@ -204,7 +205,7 @@ func (hf *HeaderPropagation) processRule(rule config.HeaderRule, index int) erro
case config.HeaderRuleOperationSet:
case config.HeaderRuleOperationPropagate:
if rule.GetMatching() != "" {
regex, err := regexp.Compile(rule.GetMatching())
regex, err := regexp.Compile(caseInsensitiveRegexp + rule.GetMatching())
if err != nil {
return fmt.Errorf("invalid regex '%s' for header rule %d: %w", rule.GetMatching(), index, err)
}
Expand Down Expand Up @@ -644,7 +645,8 @@ func PropagatedHeaders(rules []*config.RequestHeaderRule) (headerNames []string,
headerNames = append(headerNames, rule.Name)
case config.HeaderRuleOperationPropagate:
if rule.Matching != "" {
re, err := regexp.Compile(rule.Matching)
// Header Names are case insensitive: https://www.w3.org/Protocols/rfc2616/rfc2616.html
re, err := regexp.Compile(caseInsensitiveRegexp + rule.Matching)
if err != nil {
return nil, nil, fmt.Errorf("error compiling regular expression %q in header rule %+v: %w", rule.Matching, rule, err)
}
Expand Down
80 changes: 74 additions & 6 deletions router/core/header_rule_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ import (
func TestPropagateHeaderRule(t *testing.T) {

t.Run("Should propagate with named header name / named", func(t *testing.T) {

ht, err := NewHeaderPropagation(&config.HeaderRules{
All: &config.GlobalHeaderRule{
Request: []*config.RequestHeaderRule{
{
Operation: "propagate",
Named: "X-Test-1",
},
{
Operation: "propagate",
Named: "x-teST-3",
},
},
},
})
Expand All @@ -36,6 +39,7 @@ func TestPropagateHeaderRule(t *testing.T) {
require.NoError(t, err)
clientReq.Header.Set("X-Test-1", "test1")
clientReq.Header.Set("X-Test-2", "test2")
clientReq.Header.Set("X-tesT-3", "test3")

originReq, err := http.NewRequest("POST", "http://localhost", nil)
assert.Nil(t, err)
Expand All @@ -48,10 +52,10 @@ func TestPropagateHeaderRule(t *testing.T) {
subgraphResolver: NewSubgraphResolver(nil),
})

assert.Len(t, updatedClientReq.Header, 1)
assert.Len(t, updatedClientReq.Header, 2)
assert.Equal(t, "test1", updatedClientReq.Header.Get("X-Test-1"))
assert.Empty(t, updatedClientReq.Header.Get("X-Test-2"))

assert.Equal(t, "test3", updatedClientReq.Header.Get("X-Test-3"))
})

t.Run("Should propagate based on matching regex / matching", func(t *testing.T) {
Expand Down Expand Up @@ -92,6 +96,44 @@ func TestPropagateHeaderRule(t *testing.T) {
assert.Empty(t, updatedClientReq.Header.Get("Y-Test"))
})

t.Run("Should propagate based on matching regex / matching in different case", func(t *testing.T) {
ht, err := NewHeaderPropagation(&config.HeaderRules{
All: &config.GlobalHeaderRule{
Request: []*config.RequestHeaderRule{
{
Operation: "propagate",
Matching: "x-tEsT-.*",
},
},
},
})
assert.Nil(t, err)

rr := httptest.NewRecorder()

clientReq, err := http.NewRequest("POST", "http://localhost", nil)
require.NoError(t, err)
clientReq.Header.Set("x-Test-1", "test1")
clientReq.Header.Set("X-tEsT-2", "test2")
clientReq.Header.Set("Y-Test", "test3")

originReq, err := http.NewRequest("POST", "http://localhost", nil)
assert.Nil(t, err)

updatedClientReq, _ := ht.OnOriginRequest(originReq, &requestContext{
logger: zap.NewNop(),
responseWriter: rr,
request: clientReq,
operation: &operationContext{},
subgraphResolver: NewSubgraphResolver(nil),
})

assert.Len(t, updatedClientReq.Header, 2)
assert.Equal(t, "test1", updatedClientReq.Header.Get("X-Test-1"))
assert.Equal(t, "test2", updatedClientReq.Header.Get("X-Test-2"))
assert.Empty(t, updatedClientReq.Header.Get("Y-Test"))
})

t.Run("Should propagate with default value / named + default", func(t *testing.T) {
ht, err := NewHeaderPropagation(&config.HeaderRules{
All: &config.GlobalHeaderRule{
Expand Down Expand Up @@ -204,6 +246,11 @@ func TestRenamePropagateHeaderRule(t *testing.T) {
Named: "X-Test-1",
Rename: "X-Test-Renamed",
},
{
Operation: "propagate",
Named: "X-teST-cASE-insensitive",
Rename: "X-Test-case-not-sensitive",
},
},
},
})
Expand All @@ -215,6 +262,7 @@ func TestRenamePropagateHeaderRule(t *testing.T) {
require.NoError(t, err)
clientReq.Header.Set("X-Test-1", "test1")
clientReq.Header.Set("X-Test-2", "test2")
clientReq.Header.Set("X-Test-Case-Insensitive", "test3")

originReq, err := http.NewRequest("POST", "http://localhost", nil)
assert.Nil(t, err)
Expand All @@ -227,10 +275,11 @@ func TestRenamePropagateHeaderRule(t *testing.T) {
subgraphResolver: NewSubgraphResolver(nil),
})

assert.Len(t, updatedClientReq.Header, 1)
assert.Len(t, updatedClientReq.Header, 2)
assert.Equal(t, "test1", updatedClientReq.Header.Get("X-Test-Renamed"))
assert.Empty(t, updatedClientReq.Header.Get("X-Test-1"))
assert.Empty(t, updatedClientReq.Header.Get("X-Test-2"))
assert.Equal(t, "test3", updatedClientReq.Header.Get("X-Test-Case-Not-Sensitive"))
})

t.Run("Rename based on matching regex pattern / matching", func(t *testing.T) {
Expand All @@ -243,6 +292,11 @@ func TestRenamePropagateHeaderRule(t *testing.T) {
Matching: "(?i)X-Test-.*",
Rename: "X-Test-Renamed-1",
},
{
Operation: "propagate",
Matching: "x-testcase-in.*",
Rename: "X-Test-Renamed-Case",
},
{
Operation: "propagate",
Matching: "(?i)X-Test-Default-.*",
Expand All @@ -260,6 +314,7 @@ func TestRenamePropagateHeaderRule(t *testing.T) {
require.NoError(t, err)
clientReq.Header.Set("X-Test-1", "test1")
clientReq.Header.Set("X-Test-Default-2", "")
clientReq.Header.Set("x-TESTCASE-INSENSITIVE", "test3")

originReq, err := http.NewRequest("POST", "http://localhost", nil)
assert.Nil(t, err)
Expand All @@ -272,9 +327,10 @@ func TestRenamePropagateHeaderRule(t *testing.T) {
subgraphResolver: NewSubgraphResolver(nil),
})

assert.Len(t, updatedClientReq.Header, 2)
assert.Len(t, updatedClientReq.Header, 3)
assert.Equal(t, "test1", updatedClientReq.Header.Get("X-Test-Renamed-1"))
assert.Equal(t, "default", updatedClientReq.Header.Get("X-Test-Renamed-Default-2"))
assert.Equal(t, "test3", updatedClientReq.Header.Get("X-Test-Renamed-Case"))
assert.Empty(t, updatedClientReq.Header.Get("X-Test-1"))
assert.Empty(t, updatedClientReq.Header.Get("X-Test-2"))
})
Expand Down Expand Up @@ -385,6 +441,10 @@ func TestSubgraphPropagateHeaderRule(t *testing.T) {
Operation: "propagate",
Named: "X-Test-Subgraph",
},
{
Operation: "propagate",
Named: "X-test-suBGraph-case",
},
},
},
},
Expand All @@ -396,6 +456,7 @@ func TestSubgraphPropagateHeaderRule(t *testing.T) {
clientReq, err := http.NewRequest("POST", "http://localhost", nil)
require.NoError(t, err)
clientReq.Header.Set("X-Test-Subgraph", "Test-Value")
clientReq.Header.Set("X-Test-Subgraph-Case", "Test-Value1")

sg1Url, _ := url.Parse("http://subgraph-1.local")

Expand All @@ -420,8 +481,9 @@ func TestSubgraphPropagateHeaderRule(t *testing.T) {
assert.Nil(t, err)
updatedClientReq1, _ := ht.OnOriginRequest(originReq1, ctx)

assert.Len(t, updatedClientReq1.Header, 1)
assert.Len(t, updatedClientReq1.Header, 2)
assert.Equal(t, "Test-Value", updatedClientReq1.Header.Get("X-Test-Subgraph"))
assert.Equal(t, "Test-Value1", updatedClientReq1.Header.Get("X-Test-Subgraph-case"))
assert.Empty(t, updatedClientReq1.Header.Get("Test-Value"))
})

Expand All @@ -434,6 +496,10 @@ func TestSubgraphPropagateHeaderRule(t *testing.T) {
Operation: "propagate",
Matching: "(?i)X-Test-.*",
},
{
Operation: "propagate",
Matching: "X-TestCASE-.*",
},
},
},
},
Expand All @@ -445,6 +511,7 @@ func TestSubgraphPropagateHeaderRule(t *testing.T) {
clientReq, err := http.NewRequest("POST", "http://localhost", nil)
require.NoError(t, err)
clientReq.Header.Set("X-Test-Subgraph", "Test-Value")
clientReq.Header.Set("X-TestCase-Subgraph", "Test-Value")

sg1Url, _ := url.Parse("http://subgraph-1.local")

Expand All @@ -470,6 +537,7 @@ func TestSubgraphPropagateHeaderRule(t *testing.T) {
updatedClientReq1, _ := ht.OnOriginRequest(originReq1, ctx)

assert.Equal(t, "Test-Value", updatedClientReq1.Header.Get("X-Test-Subgraph"))
assert.Equal(t, "Test-Value", updatedClientReq1.Header.Get("X-TestCase-Subgraph"))
assert.Empty(t, updatedClientReq1.Header.Get("Test-Value"))
})

Expand Down

0 comments on commit c0bceaa

Please sign in to comment.