diff --git a/router-tests/go.mod b/router-tests/go.mod index dffc609754..9a23d7d64a 100644 --- a/router-tests/go.mod +++ b/router-tests/go.mod @@ -27,7 +27,7 @@ require ( github.com/wundergraph/cosmo/demo/pkg/subgraphs/projects v0.0.0-20250715110703-10f2e5f9c79e github.com/wundergraph/cosmo/router v0.0.0-20260213130455-6e3277e7b850 github.com/wundergraph/cosmo/router-plugin v0.0.0-20250808194725-de123ba1c65e - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.251 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252 go.opentelemetry.io/otel v1.36.0 go.opentelemetry.io/otel/sdk v1.36.0 go.opentelemetry.io/otel/sdk/metric v1.36.0 diff --git a/router-tests/go.sum b/router-tests/go.sum index ada9843a6a..7001df47ca 100644 --- a/router-tests/go.sum +++ b/router-tests/go.sum @@ -356,8 +356,8 @@ github.com/wundergraph/astjson v1.0.0 h1:rETLJuQkMWWW03HCF6WBttEBOu8gi5vznj5KEUP github.com/wundergraph/astjson v1.0.0/go.mod h1:h12D/dxxnedtLzsKyBLK7/Oe4TAoGpRVC9nDpDrZSWw= github.com/wundergraph/go-arena v1.1.0 h1:9+wSRkJAkA2vbYHp6s8tEGhPViRGQNGXqPHT0QzhdIc= github.com/wundergraph/go-arena v1.1.0/go.mod h1:ROOysEHWJjLQ8FSfNxZCziagb7Qw2nXY3/vgKRh7eWw= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.251 h1:avZIXjYGTLliqS+RCZXlnFAaJdEr9HizulP1qhNLR1U= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.251/go.mod h1:MFbY0QI8ncF60DHs7yyyiyyhWyld0WE1JokiyTVY8j4= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252 h1:qhm6obHtRwgZm84Gu3G63ywGz2ys2xLBwC0gloDKZuo= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252/go.mod h1:MFbY0QI8ncF60DHs7yyyiyyhWyld0WE1JokiyTVY8j4= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= diff --git a/router-tests/grpc_subgraph_test.go b/router-tests/grpc_subgraph_test.go index b09154d7a2..4871423266 100644 --- a/router-tests/grpc_subgraph_test.go +++ b/router-tests/grpc_subgraph_test.go @@ -1,11 +1,17 @@ package integration import ( + "context" + "net/http" "testing" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "github.com/wundergraph/cosmo/router-tests/testenv" + "github.com/wundergraph/cosmo/router/core" + "github.com/wundergraph/cosmo/router/pkg/config" ) func TestGRPCSubgraph(t *testing.T) { @@ -278,4 +284,347 @@ func TestGRPCSubgraph(t *testing.T) { }) }) + t.Run("Should send http headers as gRPC metadata to subgraphs", func(t *testing.T) { + t.Parallel() + + captureInterceptor := func(captured *metadata.MD) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + md, _ := metadata.FromIncomingContext(ctx) + *captured = md.Copy() + return handler(ctx, req) + } + } + + t.Run("header arrives as metadata with correct value", func(t *testing.T) { + // Assert that headers included in propagation rules are part + // of gRPC metadata. + t.Parallel() + + var captured metadata.MD + + testenv.Run(t, &testenv.Config{ + RouterConfigJSONTemplate: testenv.ConfigWithGRPCJSONTemplate, + EnableGRPC: true, + RouterOptions: []core.Option{ + core.WithHeaderRules(config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Request: []*config.RequestHeaderRule{ + { + Operation: config.HeaderRuleOperationPropagate, + Named: "X-Tenant-Id", + }, + { + Operation: config.HeaderRuleOperationPropagate, + Named: "X-Region-Name", + }, + }, + }, + }), + }, + Subgraphs: testenv.SubgraphsConfig{ + Projects: testenv.SubgraphConfig{ + GRPCInterceptor: captureInterceptor(&captured), + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { projects { id name } }`, + Header: http.Header{ + "X-Tenant-Id": []string{"acme"}, + "X-Region-Name": []string{"frankfurt"}, + }, + }) + + require.Equal(t, []string{"acme"}, captured.Get("x-tenant-id")) + require.Equal(t, []string{"frankfurt"}, captured.Get("x-region-name")) + }) + }) + + t.Run("header not in propagation rules is absent from metadata", func(t *testing.T) { + // Assert that headers not included in propagation rules are not part + // of gRPC metadata. + t.Parallel() + + var captured metadata.MD + + testenv.Run(t, &testenv.Config{ + RouterConfigJSONTemplate: testenv.ConfigWithGRPCJSONTemplate, + EnableGRPC: true, + RouterOptions: []core.Option{ + core.WithHeaderRules(config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Request: []*config.RequestHeaderRule{ + { + Operation: config.HeaderRuleOperationPropagate, + Named: "X-Allowed", + }, + }, + }, + }), + }, + Subgraphs: testenv.SubgraphsConfig{ + Projects: testenv.SubgraphConfig{ + GRPCInterceptor: captureInterceptor(&captured), + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { projects { id name } }`, + Header: http.Header{ + "X-Allowed": []string{"yes"}, + "X-Not-Allowed": []string{"secret"}, + }, + }) + + require.Equal(t, []string{"yes"}, captured.Get("x-allowed")) + require.Empty(t, captured.Get("x-not-allowed")) + }) + }) + + t.Run("header with multiple values arrives as multiple metadata values", func(t *testing.T) { + // HTTP headers can be set with multiple values. They should appear with all values + // on the gRPC metadata as well. + t.Parallel() + + var captured metadata.MD + + testenv.Run(t, &testenv.Config{ + RouterConfigJSONTemplate: testenv.ConfigWithGRPCJSONTemplate, + EnableGRPC: true, + RouterOptions: []core.Option{ + core.WithHeaderRules(config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Request: []*config.RequestHeaderRule{ + { + Operation: config.HeaderRuleOperationPropagate, + Named: "X-Role", + }, + }, + }, + }), + }, + Subgraphs: testenv.SubgraphsConfig{ + Projects: testenv.SubgraphConfig{ + GRPCInterceptor: captureInterceptor(&captured), + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { projects { id name } }`, + Header: http.Header{ + "X-Role": []string{"admin", "editor"}, + }, + }) + + require.Equal(t, []string{"admin", "editor"}, captured.Get("x-role")) + }) + }) + + t.Run("unsafe headers are absent from metadata", func(t *testing.T) { + // The router avoids passing certain headers to datasources, + // see router/core/header_rule_engine.go. + // This test ensures grpc datasources are covered by this as well. + t.Parallel() + + var captured metadata.MD + + testenv.Run(t, &testenv.Config{ + RouterConfigJSONTemplate: testenv.ConfigWithGRPCJSONTemplate, + EnableGRPC: true, + RouterOptions: []core.Option{ + core.WithHeaderRules(config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Request: []*config.RequestHeaderRule{ + { + Operation: config.HeaderRuleOperationPropagate, + Matching: ".*", // mark all headers as forwardable + }, + }, + }, + }), + }, + Subgraphs: testenv.SubgraphsConfig{ + Projects: testenv.SubgraphConfig{ + GRPCInterceptor: captureInterceptor(&captured), + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { projects { id name } }`, + Header: http.Header{ + // safe header — must arrive + "X-Custom": []string{"value"}, + + // handled by HTTP stack, never in r.Header + "Host": []string{"evil.example.com"}, + + // hop-by-hop / connection headers + "Alt-Svc": []string{"h3=\":443\""}, + "Connection": []string{"keep-alive"}, + "Keep-Alive": []string{"timeout=5"}, + "Proxy-Authenticate": []string{"Basic"}, + "Proxy-Authorization": []string{"Basic dXNlcjpwYXNz"}, + "Proxy-Connection": []string{"keep-alive"}, + "Te": []string{"trailers"}, + "Trailer": []string{"Expires"}, + "Transfer-Encoding": []string{"chunked"}, + "Upgrade": []string{"websocket"}, + + // content negotiation + "Accept": []string{"application/json"}, + "Accept-Charset": []string{"utf-8"}, + "Accept-Encoding": []string{"gzip, deflate"}, + "Content-Encoding": []string{"gzip"}, + "Content-Length": []string{"42"}, + "Content-Type": []string{"application/json"}, + + // WebSocket upgrade + "Sec-Websocket-Extensions": []string{"permessage-deflate"}, + "Sec-Websocket-Key": []string{"dGhlIHNhbXBsZSBub25jZQ=="}, + "Sec-Websocket-Protocol": []string{"chat"}, + "Sec-Websocket-Version": []string{"13"}, + }, + }) + + // custom header should arrive + require.Equal(t, []string{"value"}, captured.Get("x-custom")) + + // ensure content-type is present with the correct value + // even if the request headers have a different value + require.Equal(t, []string{"application/grpc"}, captured.Get("content-type")) + + // host is handled by the HTTP stack and never forwarded + require.Empty(t, captured.Get("host")) + + // hop-by-hop / connection headers + require.Empty(t, captured.Get("alt-svc")) + require.Empty(t, captured.Get("connection")) + require.Empty(t, captured.Get("keep-alive")) + require.Empty(t, captured.Get("proxy-authenticate")) + require.Empty(t, captured.Get("proxy-authorization")) + require.Empty(t, captured.Get("proxy-connection")) + require.Empty(t, captured.Get("te")) + require.Empty(t, captured.Get("trailer")) + require.Empty(t, captured.Get("transfer-encoding")) + require.Empty(t, captured.Get("upgrade")) + + // content negotiation + require.Empty(t, captured.Get("accept")) + require.Empty(t, captured.Get("accept-charset")) + require.Empty(t, captured.Get("accept-encoding")) + require.Empty(t, captured.Get("content-encoding")) + require.Empty(t, captured.Get("content-length")) + + // WebSocket upgrade + require.Empty(t, captured.Get("sec-websocket-extensions")) + require.Empty(t, captured.Get("sec-websocket-key")) + require.Empty(t, captured.Get("sec-websocket-protocol")) + require.Empty(t, captured.Get("sec-websocket-version")) + + // gRPC client correctness: + // HTTP/2 pseudo-headers — the gRPC transport explicitly whitelists + // :authority and user-agent so they appear in metadata. + require.NotEmpty(t, captured.Get(":authority")) + require.NotEmpty(t, captured.Get("user-agent")) + + // gRPC client correctness: + // All other pseudo-headers (:method, :path, :scheme) are classified + // as reserved and stripped by the gRPC transport before reaching + // user-space, so they never appear regardless of router rules. + require.Empty(t, captured.Get(":method")) + require.Empty(t, captured.Get(":path")) + require.Empty(t, captured.Get(":scheme")) + }) + }) + + t.Run("grpc-reserved headers never reach the subgraph", func(t *testing.T) { + // Headers prefixed with "grpc-" are reserved by the gRPC protocol spec. + // Even when wildcard propagation is configured, they must never appear + // on the subgraph. + t.Parallel() + + var captured metadata.MD + + testenv.Run(t, &testenv.Config{ + RouterConfigJSONTemplate: testenv.ConfigWithGRPCJSONTemplate, + EnableGRPC: true, + RouterOptions: []core.Option{ + core.WithHeaderRules(config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Request: []*config.RequestHeaderRule{ + { + Operation: config.HeaderRuleOperationPropagate, + Matching: ".*", + }, + }, + }, + }), + }, + Subgraphs: testenv.SubgraphsConfig{ + Projects: testenv.SubgraphConfig{ + GRPCInterceptor: captureInterceptor(&captured), + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { projects { id name } }`, + Header: http.Header{ + "Grpc-ReservedHeader": []string{"should be ignored"}, + }, + }) + + require.Empty(t, captured.Get("grpc-reservedheader")) + }) + }) + + t.Run("safe headers are present in metadata", func(t *testing.T) { + // Ensure that http standard headers, which are safe and useful for subgraphs, + // are included in the metadata unmodified. + t.Parallel() + + var captured metadata.MD + + testenv.Run(t, &testenv.Config{ + RouterConfigJSONTemplate: testenv.ConfigWithGRPCJSONTemplate, + EnableGRPC: true, + RouterOptions: []core.Option{ + core.WithHeaderRules(config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Request: []*config.RequestHeaderRule{ + // Propagate each header explicitly so the test is + // independent of any default propagation behaviour. + {Operation: config.HeaderRuleOperationPropagate, Named: "Authorization"}, + {Operation: config.HeaderRuleOperationPropagate, Named: "Cookie"}, + {Operation: config.HeaderRuleOperationPropagate, Named: "Traceparent"}, + {Operation: config.HeaderRuleOperationPropagate, Named: "Tracestate"}, + {Operation: config.HeaderRuleOperationPropagate, Named: "Accept-Language"}, + }, + }, + }), + }, + Subgraphs: testenv.SubgraphsConfig{ + Projects: testenv.SubgraphConfig{ + GRPCInterceptor: captureInterceptor(&captured), + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `query { projects { id name } }`, + Header: http.Header{ + "Authorization": []string{"Bearer eyJhbGciOiJSUzI1NiJ9"}, + "Cookie": []string{"session=abc123; theme=dark"}, + "Traceparent": []string{"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}, + "Tracestate": []string{"rojo=00f067aa0ba902b7"}, + "Accept-Language": []string{"de-DE,de;q=0.9,en;q=0.8"}, + }, + }) + + require.Equal(t, []string{"Bearer eyJhbGciOiJSUzI1NiJ9"}, captured.Get("authorization")) + require.Equal(t, []string{"session=abc123; theme=dark"}, captured.Get("cookie")) + require.Equal(t, []string{"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}, captured.Get("traceparent")) + require.Equal(t, []string{"rojo=00f067aa0ba902b7"}, captured.Get("tracestate")) + require.Equal(t, []string{"de-DE,de;q=0.9,en;q=0.8"}, captured.Get("accept-language")) + }) + }) + }) } diff --git a/router-tests/integration_test.go b/router-tests/integration_test.go index be8fc6c5aa..fd37015572 100644 --- a/router-tests/integration_test.go +++ b/router-tests/integration_test.go @@ -1763,3 +1763,45 @@ func TestDataNotSetOnPreExecutionErrors(t *testing.T) { require.Equal(t, `{"errors":[{"message":"unexpected token - got: RBRACE want one of: [COLON]","locations":[{"line":1,"column":46}]}]}`, res.Body) }) } + +func TestNullParentSkippedFetch(t *testing.T) { + t.Parallel() + + t.Run("no panic when parent entity is null and child fetch is skipped", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { + // employee(id: 999) does not exist — returns null from the employees subgraph. + // currentMood is resolved from the mood subgraph via entity resolution. + // With a null parent, the mood fetch is skipped. + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `{ employee(id: 999) { id currentMood } }`, + }) + require.Equal(t, `{"data":{"employee":null}}`, res.Body) + }) + }) + + t.Run("no panic when parent entity is null with response header propagation", func(t *testing.T) { + // Reproduces issue #2530: OnFinished → ApplyResponseHeaderRules → + // getResponseHeaderPropagation panics on nil context when fetches are skipped. + t.Parallel() + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithHeaderRules(config.HeaderRules{ + All: &config.GlobalHeaderRule{ + Response: []*config.ResponseHeaderRule{ + { + Operation: config.HeaderRuleOperationPropagate, + Algorithm: config.ResponseHeaderRuleAlgorithmMostRestrictiveCacheControl, + }, + }, + }, + }), + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `{ employee(id: 999) { id currentMood } }`, + }) + require.Equal(t, `{"data":{"employee":null}}`, res.Body) + }) + }) +} diff --git a/router-tests/testenv/testenv.go b/router-tests/testenv/testenv.go index 2100bdd203..0b9b9b2806 100644 --- a/router-tests/testenv/testenv.go +++ b/router-tests/testenv/testenv.go @@ -388,9 +388,10 @@ type SubgraphsConfig struct { } type SubgraphConfig struct { - Middleware func(http.Handler) http.Handler - Delay time.Duration - CloseOnStart bool + Middleware func(http.Handler) http.Handler + GRPCInterceptor grpc.UnaryServerInterceptor + Delay time.Duration + CloseOnStart bool } type LogObservationConfig struct { @@ -597,7 +598,7 @@ func CreateTestSupervisorEnv(t testing.TB, cfg *Config) (*Environment, error) { ) if cfg.EnableGRPC { - projectServer, endpoint = makeSafeGRPCServer(t, &projects.ProjectsService_ServiceDesc, &service.ProjectsService{}) + projectServer, endpoint = makeSafeGRPCServer(t, &projects.ProjectsService_ServiceDesc, &service.ProjectsService{}, cfg.Subgraphs.Projects.GRPCInterceptor) } replacements := map[string]string{ @@ -1027,7 +1028,7 @@ func CreateTestEnv(t testing.TB, cfg *Config) (*Environment, error) { ) if cfg.EnableGRPC { - projectServer, endpoint = makeSafeGRPCServer(t, &projects.ProjectsService_ServiceDesc, &service.ProjectsService{}) + projectServer, endpoint = makeSafeGRPCServer(t, &projects.ProjectsService_ServiceDesc, &service.ProjectsService{}, cfg.Subgraphs.Projects.GRPCInterceptor) } replacements := map[string]string{ @@ -1673,7 +1674,7 @@ func makeSafeHttpTestServer(t testing.TB, handler http.Handler) *httptest.Server return s } -func makeSafeGRPCServer(t testing.TB, sd *grpc.ServiceDesc, service any) (*grpc.Server, string) { +func makeSafeGRPCServer(t testing.TB, sd *grpc.ServiceDesc, service any, interceptor grpc.UnaryServerInterceptor) (*grpc.Server, string) { t.Helper() // We could use freeport here, but it is easy to use ephemeral port and get the endpoint @@ -1684,7 +1685,12 @@ func makeSafeGRPCServer(t testing.TB, sd *grpc.ServiceDesc, service any) (*grpc. require.NotNil(t, service) - s := grpc.NewServer() + var opts []grpc.ServerOption + if interceptor != nil { + opts = append(opts, grpc.ChainUnaryInterceptor(interceptor)) + } + + s := grpc.NewServer(opts...) s.RegisterService(sd, service) go func() { diff --git a/router/core/header_rule_engine.go b/router/core/header_rule_engine.go index 3d825fdb21..bd7635a65d 100644 --- a/router/core/header_rule_engine.go +++ b/router/core/header_rule_engine.go @@ -14,7 +14,7 @@ import ( "time" "github.com/cespare/xxhash/v2" - "github.com/expr-lang/expr/vm" + "github.com/expr-lang/expr/vm" cachedirective "github.com/pquerna/cachecontrol/cacheobject" nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" "github.com/wundergraph/cosmo/router/internal/expr" @@ -31,13 +31,34 @@ import ( ) var ( - _ EnginePostOriginHandler = (*HeaderPropagation)(nil) - cacheControlKey = "Cache-Control" - expiresKey = "Expires" - noCache = "no-cache" - caseInsensitiveRegexp = "(?i)" + _ EnginePostOriginHandler = (*HeaderPropagation)(nil) + cacheControlKey = "Cache-Control" + expiresKey = "Expires" + noCache = "no-cache" + caseInsensitiveRegexp = "(?i)" ) +// ignoredHeaderPrefixes are prefixes for headers that should not be forwarded to downstream services. +var ignoredHeaderPrefixes = []string{ + "Grpc-", // reserved in gRPC metadata +} + +// isIgnoredHeader reports whether a header should never be propagated to subgraphs. +// It checks both the exact ignoredHeaders list and any prefix in ignoredHeaderPrefixes. +func isIgnoredHeader(name string) bool { + canonicalName := http.CanonicalHeaderKey(name) + + if _, ok := headers.SkippedHeaders[canonicalName]; ok { + return true + } + for _, prefix := range ignoredHeaderPrefixes { + if strings.HasPrefix(canonicalName, prefix) { + return true + } + } + return false +} + type responseHeaderPropagationKey struct{} type responseHeaderPropagation struct { @@ -444,7 +465,7 @@ func (h *HeaderPropagation) applyResponseRule(propagation *responseHeaderPropaga } if rule.Named != "" { - if _, ok := headers.SkippedHeaders[rule.Named]; ok { + if isIgnoredHeader(rule.Named) { return } @@ -464,7 +485,7 @@ func (h *HeaderPropagation) applyResponseRule(propagation *responseHeaderPropaga result = !result } if result { - if _, ok := headers.SkippedHeaders[name]; ok { + if isIgnoredHeader(name) { continue } values := res.Header.Values(name) @@ -544,7 +565,7 @@ func (h *HeaderPropagation) applyRequestRuleToHeader(ctx *requestContext, header if rule.Rename != "" && rule.Named != "" { // Ignore the rule when the target header is in the ignored list - if _, ok := headers.SkippedHeaders[rule.Rename]; ok { + if isIgnoredHeader(rule.Rename) { return } @@ -567,7 +588,7 @@ func (h *HeaderPropagation) applyRequestRuleToHeader(ctx *requestContext, header */ if rule.Named != "" { - if _, ok := headers.SkippedHeaders[rule.Named]; ok { + if isIgnoredHeader(rule.Named) { return } @@ -600,7 +621,7 @@ func (h *HeaderPropagation) applyRequestRuleToHeader(ctx *requestContext, header */ if rule.Rename != "" && rule.Named == "" { - if _, ok := headers.SkippedHeaders[rule.Rename]; ok { + if isIgnoredHeader(rule.Rename) { continue } @@ -619,7 +640,7 @@ func (h *HeaderPropagation) applyRequestRuleToHeader(ctx *requestContext, header /** * Propagate the header as is */ - if _, ok := headers.SkippedHeaders[name]; ok { + if isIgnoredHeader(name) { continue } header.Set(name, ctx.Request().Header.Get(name)) diff --git a/router/go.mod b/router/go.mod index 6d412173e5..3ae4a9da22 100644 --- a/router/go.mod +++ b/router/go.mod @@ -31,7 +31,7 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 github.com/twmb/franz-go v1.16.1 - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.251 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252 // Do not upgrade, it renames attributes we rely on go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 go.opentelemetry.io/contrib/propagators/b3 v1.23.0 diff --git a/router/go.sum b/router/go.sum index a1885d483d..a51d0b4dbd 100644 --- a/router/go.sum +++ b/router/go.sum @@ -326,8 +326,8 @@ github.com/wundergraph/astjson v1.0.0 h1:rETLJuQkMWWW03HCF6WBttEBOu8gi5vznj5KEUP github.com/wundergraph/astjson v1.0.0/go.mod h1:h12D/dxxnedtLzsKyBLK7/Oe4TAoGpRVC9nDpDrZSWw= github.com/wundergraph/go-arena v1.1.0 h1:9+wSRkJAkA2vbYHp6s8tEGhPViRGQNGXqPHT0QzhdIc= github.com/wundergraph/go-arena v1.1.0/go.mod h1:ROOysEHWJjLQ8FSfNxZCziagb7Qw2nXY3/vgKRh7eWw= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.251 h1:avZIXjYGTLliqS+RCZXlnFAaJdEr9HizulP1qhNLR1U= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.251/go.mod h1:MFbY0QI8ncF60DHs7yyyiyyhWyld0WE1JokiyTVY8j4= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252 h1:qhm6obHtRwgZm84Gu3G63ywGz2ys2xLBwC0gloDKZuo= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.252/go.mod h1:MFbY0QI8ncF60DHs7yyyiyyhWyld0WE1JokiyTVY8j4= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= diff --git a/router/internal/headers/headers.go b/router/internal/headers/headers.go index 8dd14beaa5..936ba33cc7 100644 --- a/router/internal/headers/headers.go +++ b/router/internal/headers/headers.go @@ -4,19 +4,19 @@ package headers // These headers are connection-specific or should be set by the client/server // rather than being forwarded from the original request. var SkippedHeaders = map[string]struct{}{ - "Alt-Svc": {}, - "Connection": {}, - "Proxy-Connection": {}, // non-standard but still sent by libcurl and rejected by e.g. google + "Alt-Svc": {}, + "Connection": {}, + "Proxy-Connection": {}, // non-standard but still sent by libcurl and rejected by e.g. google // Hop-by-hop headers // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection - "Keep-Alive": {}, - "Proxy-Authenticate": {}, + "Keep-Alive": {}, + "Proxy-Authenticate": {}, "Proxy-Authorization": {}, - "Te": {}, // canonicalized version of "TE" - "Trailer": {}, // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522 - "Transfer-Encoding": {}, - "Upgrade": {}, + "Te": {}, // canonicalized version of "TE" + "Trailer": {}, // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522 + "Transfer-Encoding": {}, + "Upgrade": {}, // 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