Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
151 changes: 151 additions & 0 deletions router-tests/header_propagation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,157 @@ func TestHeaderPropagation(t *testing.T) {
require.Equal(t, `{"data":{"employee":{"id":1,"hobbies":[{},{"name":"Counter Strike"},{},{},{}]}}}`, res.Body)
})
})

// Tests that verify the append algorithm produces a SINGLE header with
// comma-separated values, not multiple separate headers (issue #2531).
t.Run("global append produces single comma-separated header", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: global(config.ResponseHeaderRuleAlgorithmAppend, customHeader, ""),
Subgraphs: subgraphsPropagateCustomHeader,
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: queryEmployeeWithHobby,
})
values := res.Response.Header.Values(customHeader)
require.Equal(t, 1, len(values),
"append algorithm should produce a single header with comma-separated values, got %d entries: %v", len(values), values)
require.Equal(t, "employee-value,hobby-value", values[0])
})
})

t.Run("local append produces single comma-separated header", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: local(config.ResponseHeaderRuleAlgorithmAppend, customHeader, "", ""),
Subgraphs: subgraphsPropagateCustomHeader,
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: queryEmployeeWithHobby,
})
values := res.Response.Header.Values(customHeader)
require.Equal(t, 1, len(values),
"append algorithm should produce a single header with comma-separated values, got %d entries: %v", len(values), values)
require.Equal(t, "employee-value,hobby-value", values[0])
})
})

t.Run("repeated header names append produces single comma-separated header", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: global(config.ResponseHeaderRuleAlgorithmAppend, customHeader, ""),
Subgraphs: subgraphsPropagateRepeatedCustomHeader,
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: queryEmployeeWithHobby,
})
values := res.Response.Header.Values(customHeader)
require.Equal(t, 1, len(values),
"append algorithm should produce a single header with comma-separated values, got %d entries: %v", len(values), values)
require.Equal(t, "employee-value,employee-value-2,hobby-value,hobby-value-2", values[0])
})
})

t.Run("append with default value produces single header", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: global(config.ResponseHeaderRuleAlgorithmAppend, customHeader, "default-val"),
Subgraphs: testenv.SubgraphsConfig{
Employees: testenv.SubgraphConfig{
Middleware: func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header()[customHeader] = []string{employeeVal}
handler.ServeHTTP(w, r)
})
},
},
// Hobbies does NOT set the header — the default should be used
},
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: queryEmployeeWithHobby,
})
values := res.Response.Header.Values(customHeader)
require.Equal(t, 1, len(values),
"append algorithm should produce a single header with comma-separated values, got %d entries: %v", len(values), values)
require.Equal(t, "employee-value,default-val", values[0])
})
})
Comment thread
endigma marked this conversation as resolved.

t.Run("append with regex matching produces single comma-separated header", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithHeaderRules(config.HeaderRules{
All: &config.GlobalHeaderRule{
Response: []*config.ResponseHeaderRule{
{
Operation: config.HeaderRuleOperationPropagate,
Matching: "^X-Custom-Header$",
Algorithm: config.ResponseHeaderRuleAlgorithmAppend,
},
},
},
}),
},
Subgraphs: subgraphsPropagateCustomHeader,
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: queryEmployeeWithHobby,
})
values := res.Response.Header.Values(customHeader)
require.Equal(t, 1, len(values),
"append algorithm should produce a single header with comma-separated values, got %d entries: %v", len(values), values)
Comment thread
jensneuse marked this conversation as resolved.
Outdated
require.Equal(t, "employee-value,hobby-value", values[0])
})
})
})

// Tests for default value fallback when a subgraph does not return the header
t.Run("DefaultValue", func(t *testing.T) {
t.Parallel()

subgraphsOnlyEmployeeSetsHeader := testenv.SubgraphsConfig{
Employees: testenv.SubgraphConfig{
Middleware: func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header()[customHeader] = []string{employeeVal}
handler.ServeHTTP(w, r)
})
},
},
// Hobbies does NOT set the header
}

t.Run("last write with default uses default from non-responding subgraph", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: global(config.ResponseHeaderRuleAlgorithmLastWrite, customHeader, "default-val"),
Subgraphs: subgraphsOnlyEmployeeSetsHeader,
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: queryEmployeeWithHobby,
})
ch := res.Response.Header.Get(customHeader)
// Hobbies responds last and uses the default value
require.Equal(t, "default-val", ch)
})
})
Comment thread
endigma marked this conversation as resolved.

t.Run("first write with default keeps first value", func(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: global(config.ResponseHeaderRuleAlgorithmFirstWrite, customHeader, "default-val"),
Subgraphs: subgraphsOnlyEmployeeSetsHeader,
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: queryEmployeeWithHobby,
})
ch := res.Response.Header.Get(customHeader)
// Employees responds first with its actual value
require.Equal(t, employeeVal, ch)
})
})
})

t.Run("Cache Control Propagation", func(t *testing.T) {
Expand Down
8 changes: 7 additions & 1 deletion router/core/header_rule_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,13 @@ func (h *HeaderPropagation) applyResponseRuleKeyValue(res *http.Response, propag
propagation.m.Unlock()
case config.ResponseHeaderRuleAlgorithmAppend:
propagation.m.Lock()
propagation.header[key] = append(propagation.header[key], values...)
existing := propagation.header.Get(key)
Comment thread
jensneuse marked this conversation as resolved.
Outdated
newVal := strings.Join(values, ",")
Comment thread
jensneuse marked this conversation as resolved.
Outdated
if existing != "" {
propagation.header.Set(key, existing+","+newVal)
} else {
propagation.header.Set(key, newVal)
}
propagation.m.Unlock()
case config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl:
h.applyResponseRuleMostRestrictiveCacheControl(res, propagation, rule)
Expand Down
Loading
Loading