Skip to content
Merged
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
4 changes: 2 additions & 2 deletions demo/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
Expand Down
122 changes: 122 additions & 0 deletions router-tests/header_propagation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,128 @@ func TestCacheControl(t *testing.T) {
assert.Equal(t, `{"errors":[{"message":"Failed to fetch from Subgraph 'employees'.","extensions":{"errors":[{"message":"error resolving RootFieldThrowsError for Employee 1","path":["employee","rootFieldThrowsError"],"extensions":{"code":"ERROR_CODE"}}],"statusCode":200}}],"data":{"employee":{"id":1,"rootFieldThrowsError":null}}}`, res.Body)
})
})

t.Run("Ignored response headers from subgraphs are never propagated", func(t *testing.T) {
t.Parallel()

// Test that subgraph response headers in the ignoredHeaders list are never propagated to client,
// even when propagation rules are configured. The router manages these headers itself.
testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithHeaderRules(config.HeaderRules{
All: &config.GlobalHeaderRule{
Response: []*config.ResponseHeaderRule{
{
Operation: config.HeaderRuleOperationPropagate,
Named: "Content-Type",
Algorithm: config.ResponseHeaderRuleAlgorithmLastWrite,
},
{
Operation: config.HeaderRuleOperationPropagate,
Named: "Content-Encoding",
Algorithm: config.ResponseHeaderRuleAlgorithmLastWrite,
},
{
Operation: config.HeaderRuleOperationPropagate,
Named: "Connection",
Algorithm: config.ResponseHeaderRuleAlgorithmLastWrite,
},
{
Operation: config.HeaderRuleOperationPropagate,
Named: "X-Custom-Header",
Algorithm: config.ResponseHeaderRuleAlgorithmLastWrite,
},
},
},
}),
},
Subgraphs: testenv.SubgraphsConfig{
Employees: testenv.SubgraphConfig{
Middleware: func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Attempt to set ignored headers - these should NOT be propagated
w.Header().Set("Content-Type", "application/custom-from-subgraph")
w.Header().Set("Content-Encoding", "gzip-from-subgraph")
w.Header().Set("Connection", "keep-alive-from-subgraph")
// This should be propagated
w.Header().Set("X-Custom-Header", "custom-value")
handler.ServeHTTP(w, r)
})
},
},
},
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `{ employee(id: 1) { id } }`,
})

// Verify subgraph's ignored headers are NOT propagated to client
contentType := res.Response.Header.Get("Content-Type")
require.NotEqual(t, "application/custom-from-subgraph", contentType, "Subgraph Content-Type should not be propagated")

contentEncoding := res.Response.Header.Get("Content-Encoding")
require.NotEqual(t, "gzip-from-subgraph", contentEncoding, "Subgraph Content-Encoding should not be propagated")

connection := res.Response.Header.Get("Connection")
require.NotEqual(t, "keep-alive-from-subgraph", connection, "Subgraph Connection should not be propagated")

// Verify custom header IS propagated (not in ignored list)
require.Equal(t, "custom-value", res.Response.Header.Get("X-Custom-Header"))
})
})

t.Run("Ignored response headers with regex matching are never propagated from subgraphs", func(t *testing.T) {
t.Parallel()

// Test that subgraph response headers in the ignoredHeaders list are not propagated
// even with regex matching rules
testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithHeaderRules(config.HeaderRules{
All: &config.GlobalHeaderRule{
Response: []*config.ResponseHeaderRule{
{
Operation: config.HeaderRuleOperationPropagate,
Matching: "^Content-.*", // Should match Content-Type, Content-Encoding, Content-Length
Algorithm: config.ResponseHeaderRuleAlgorithmLastWrite,
},
{
Operation: config.HeaderRuleOperationPropagate,
Matching: ".*", // Match all headers
Algorithm: config.ResponseHeaderRuleAlgorithmLastWrite,
},
},
},
}),
},
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().Set("Content-Type", "application/custom-from-subgraph")
w.Header().Set("Content-Encoding", "gzip-from-subgraph")
w.Header().Set("X-Custom-Header", "should-be-propagated")
handler.ServeHTTP(w, r)
})
},
},
},
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: `{ employee(id: 1) { id } }`,
})

// Content-* headers from subgraph should NOT be propagated to client (router manages these)
contentType := res.Response.Header.Get("Content-Type")
require.NotEqual(t, "application/custom-from-subgraph", contentType, "Subgraph Content-Type should not be propagated")

contentEncoding := res.Response.Header.Get("Content-Encoding")
require.NotEqual(t, "gzip-from-subgraph", contentEncoding, "Subgraph Content-Encoding should not be propagated")

// X-Custom-Header SHOULD be propagated (not in ignored list)
require.Equal(t, "should-be-propagated", res.Response.Header.Get("X-Custom-Header"))
})
})
}

func TestHeaderPropagation(t *testing.T) {
Expand Down
131 changes: 131 additions & 0 deletions router-tests/headers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,137 @@ func TestForwardHeaders(t *testing.T) {
}
})
})

t.Run("Ignored headers are never forwarded from client", func(t *testing.T) {
t.Parallel()

// Test that client headers in the ignoredHeaders list are never forwarded to subgraphs,
// even when propagation rules are configured. The router manages these headers itself.
testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithHeaderRules(config.HeaderRules{
All: &config.GlobalHeaderRule{
Request: []*config.RequestHeaderRule{
{
Operation: config.HeaderRuleOperationPropagate,
Named: "Content-Type",
},
{
Operation: config.HeaderRuleOperationPropagate,
Named: "Content-Encoding",
},
{
Operation: config.HeaderRuleOperationPropagate,
Named: "Connection",
},
{
Operation: config.HeaderRuleOperationPropagate,
Named: "Transfer-Encoding",
},
{
Operation: config.HeaderRuleOperationPropagate,
Named: "X-Custom-Header",
},
},
},
}),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Header: http.Header{
"Content-Type": []string{"application/custom-from-client"},
"Content-Encoding": []string{"gzip-from-client"},
"Connection": []string{"keep-alive-from-client"},
"Transfer-Encoding": []string{"chunked-from-client"},
"X-Custom-Header": []string{"should-be-forwarded"},
},
Query: `query {
contentType: headerValue(name:"Content-Type"),
contentEncoding: headerValue(name:"Content-Encoding"),
connection: headerValue(name:"Connection"),
transferEncoding: headerValue(name:"Transfer-Encoding"),
customHeader: headerValue(name:"X-Custom-Header")
}`,
})

// Parse the response to check individual values
var result struct {
Data struct {
ContentType string `json:"contentType"`
ContentEncoding string `json:"contentEncoding"`
Connection string `json:"connection"`
TransferEncoding string `json:"transferEncoding"`
CustomHeader string `json:"customHeader"`
} `json:"data"`
}
err := json.Unmarshal([]byte(res.Body), &result)
require.NoError(t, err)

// Client's ignored headers should NOT be forwarded (empty or router-managed values)
require.NotEqual(t, "application/custom-from-client", result.Data.ContentType, "Client Content-Type should not be forwarded")
require.NotEqual(t, "gzip-from-client", result.Data.ContentEncoding, "Client Content-Encoding should not be forwarded")
require.NotEqual(t, "keep-alive-from-client", result.Data.Connection, "Client Connection should not be forwarded")
require.NotEqual(t, "chunked-from-client", result.Data.TransferEncoding, "Client Transfer-Encoding should not be forwarded")

// Non-ignored headers SHOULD be forwarded
require.Equal(t, "should-be-forwarded", result.Data.CustomHeader, "Custom header should be forwarded")
})
})

t.Run("Ignored headers with regex matching are never forwarded from client", func(t *testing.T) {
t.Parallel()

// Test that client headers in the ignoredHeaders list are never forwarded even with regex matching
testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithHeaderRules(config.HeaderRules{
All: &config.GlobalHeaderRule{
Request: []*config.RequestHeaderRule{
{
Operation: config.HeaderRuleOperationPropagate,
Matching: "^Content-.*", // Should match Content-Type, Content-Encoding, Content-Length
},
{
Operation: config.HeaderRuleOperationPropagate,
Matching: ".*", // Match all headers
},
},
},
}),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Header: http.Header{
"Content-Type": []string{"application/custom-from-client"},
"Content-Encoding": []string{"gzip-from-client"},
"X-Custom-Header": []string{"should-be-forwarded"},
},
Query: `query {
contentType: headerValue(name:"Content-Type"),
contentEncoding: headerValue(name:"Content-Encoding"),
customHeader: headerValue(name:"X-Custom-Header")
}`,
})

// Parse the response
var result struct {
Data struct {
ContentType string `json:"contentType"`
ContentEncoding string `json:"contentEncoding"`
CustomHeader string `json:"customHeader"`
} `json:"data"`
}
err := json.Unmarshal([]byte(res.Body), &result)
require.NoError(t, err)

// Client's Content-* headers should NOT be forwarded (router manages these)
require.NotEqual(t, "application/custom-from-client", result.Data.ContentType, "Client Content-Type should not be forwarded")
require.NotEqual(t, "gzip-from-client", result.Data.ContentEncoding, "Client Content-Encoding should not be forwarded")

// X-Custom-Header SHOULD be forwarded (not in ignored list)
require.Equal(t, "should-be-forwarded", result.Data.CustomHeader, "Custom header should be forwarded")
})
})
}

func TestForwardRenamedHeaders(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion router/core/header_rule_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package core
import (
"context"
"fmt"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/graphql_datasource"
"io"
"net/http"
"reflect"
Expand All @@ -13,6 +12,8 @@ import (
"sync"
"time"

"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/graphql_datasource"

"github.com/expr-lang/expr/vm"
cachedirective "github.com/pquerna/cachecontrol/cacheobject"
nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1"
Expand Down Expand Up @@ -45,6 +46,8 @@ var (
// Content Negotiation. We must never propagate the client headers to the upstream
// The router has to decide on its own what to send to the upstream
"Content-Type",
"Content-Encoding",
"Content-Length",
"Accept-Encoding",
"Accept-Charset",
"Accept",
Expand Down
Loading