diff --git a/router-tests/error_handling_test.go b/router-tests/error_handling_test.go index e5bd1ca944..feca3791a8 100644 --- a/router-tests/error_handling_test.go +++ b/router-tests/error_handling_test.go @@ -219,6 +219,121 @@ func TestFallbackErrors(t *testing.T) { }) } +func TestAllowedExtensions(t *testing.T) { + t.Parallel() + + t.Run("in wrapped mode, only allowed extensions should be included in the propagated error", func(t *testing.T) { + testenv.Run(t, &testenv.Config{ + ModifySubgraphErrorPropagation: func(cfg *config.SubgraphErrorPropagationConfiguration) { + cfg.Enabled = true + cfg.Mode = config.SubgraphErrorPropagationModeWrapped + cfg.AllowedExtensionFields = []string{"allowed"} + }, + 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/json") + w.WriteHeader(http.StatusForbidden) + _, wErr := w.Write([]byte(`{"errors":[{"message":"Unauthorized","extensions":{"allowed":"allowed","notAllowed":"notAllowed"}}]}`)) + require.NoError(t, wErr) + }) + }, + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `{ employees { id } }`, + }) + require.Equal(t, `{"errors":[{"message":"Failed to fetch from Subgraph 'employees'.","extensions":{"errors":[{"message":"Unauthorized","extensions":{"allowed":"allowed"}}],"statusCode":403}}],"data":{"employees":null}}`, res.Body) + }) + }) + + t.Run("in wrapped mode, with AllowAllExtensionFields set, all extensions should be included in the propagated error", func(t *testing.T) { + testenv.Run(t, &testenv.Config{ + ModifySubgraphErrorPropagation: func(cfg *config.SubgraphErrorPropagationConfiguration) { + cfg.Enabled = true + cfg.Mode = config.SubgraphErrorPropagationModeWrapped + cfg.AllowedExtensionFields = []string{"allowed"} + cfg.AllowAllExtensionFields = true + }, + 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/json") + w.WriteHeader(http.StatusForbidden) + _, wErr := w.Write([]byte(`{"errors":[{"message":"Unauthorized","extensions":{"allowed":"allowed","notAllowed":"notAllowed"}}]}`)) + require.NoError(t, wErr) + }) + }, + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `{ employees { id } }`, + }) + require.Equal(t, `{"errors":[{"message":"Failed to fetch from Subgraph 'employees'.","extensions":{"errors":[{"message":"Unauthorized","extensions":{"allowed":"allowed","notAllowed":"notAllowed"}}],"statusCode":403}}],"data":{"employees":null}}`, res.Body) + }) + }) + + t.Run("in passthrough mode, only allowed extensions should be included in the propagated error", func(t *testing.T) { + testenv.Run(t, &testenv.Config{ + ModifySubgraphErrorPropagation: func(cfg *config.SubgraphErrorPropagationConfiguration) { + cfg.Enabled = true + cfg.Mode = config.SubgraphErrorPropagationModePassthrough + cfg.AllowedExtensionFields = []string{"allowed"} + }, + 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/json") + w.WriteHeader(http.StatusForbidden) + _, wErr := w.Write([]byte(`{"errors":[{"message":"Unauthorized","extensions":{"allowed":"allowed","notAllowed":"notAllowed"}}]}`)) + require.NoError(t, wErr) + }) + }, + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `{ employees { id } }`, + }) + require.Equal(t, `{"errors":[{"message":"Unauthorized","extensions":{"allowed":"allowed","statusCode":403}}],"data":{"employees":null}}`, res.Body) + }) + }) + + t.Run("in passthrough mode, with AllowAllExtensionFields set, all extensions should be included in the propagated error", func(t *testing.T) { + testenv.Run(t, &testenv.Config{ + ModifySubgraphErrorPropagation: func(cfg *config.SubgraphErrorPropagationConfiguration) { + cfg.Enabled = true + cfg.Mode = config.SubgraphErrorPropagationModePassthrough + cfg.AllowedExtensionFields = []string{"allowed"} + cfg.AllowAllExtensionFields = true + }, + 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/json") + w.WriteHeader(http.StatusForbidden) + _, wErr := w.Write([]byte(`{"errors":[{"message":"Unauthorized","extensions":{"allowed":"allowed","notAllowed":"notAllowed"}}]}`)) + require.NoError(t, wErr) + }) + }, + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `{ employees { id } }`, + }) + require.Equal(t, `{"errors":[{"message":"Unauthorized","extensions":{"allowed":"allowed","notAllowed":"notAllowed","statusCode":403}}],"data":{"employees":null}}`, res.Body) + }) + }) + +} + func TestErrorPropagation(t *testing.T) { t.Parallel() diff --git a/router-tests/go.mod b/router-tests/go.mod index 2dfe30051e..1aed48f8a1 100644 --- a/router-tests/go.mod +++ b/router-tests/go.mod @@ -25,7 +25,7 @@ require ( github.com/wundergraph/cosmo/demo v0.0.0-20250707145555-35d60cac85d9 github.com/wundergraph/cosmo/demo/pkg/subgraphs/projects v0.0.0-20250616075713-f2b99c96cec4 github.com/wundergraph/cosmo/router v0.0.0-20250707145555-35d60cac85d9 - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.202 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.203 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/sdk/metric v1.28.0 diff --git a/router-tests/go.sum b/router-tests/go.sum index 7836549974..463085725f 100644 --- a/router-tests/go.sum +++ b/router-tests/go.sum @@ -326,8 +326,8 @@ github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTB github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= github.com/wundergraph/consul/sdk v0.0.0-20250204115147-ed842a8fd301 h1:EzfKHQoTjFDDcgaECCCR2aTePqMu9QBmPbyhqIYOhV0= github.com/wundergraph/consul/sdk v0.0.0-20250204115147-ed842a8fd301/go.mod h1:wxI0Nak5dI5RvJuzGyiEK4nZj0O9X+Aw6U0tC1wPKq0= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.202 h1:C0rQNddwMMou4o1iXHhXiOlY6/IUF2yS9Pw3u3hd+ts= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.202/go.mod h1:DaBrBCMgKGd3t7zg7z11jKm+0mVJiesr/IQCRG9qgP0= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.203 h1:qTMYS9EICDCoMY90ILE3eW2/i1VNMhmyl79qpw5v6xc= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.203/go.mod h1:DaBrBCMgKGd3t7zg7z11jKm+0mVJiesr/IQCRG9qgP0= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= diff --git a/router/core/executor.go b/router/core/executor.go index 0cb9b9febb..560f40840e 100644 --- a/router/core/executor.go +++ b/router/core/executor.go @@ -81,6 +81,7 @@ func (b *ExecutorConfigurationBuilder) Build(ctx context.Context, opts *Executor AttachServiceNameToErrorExtensions: opts.RouterEngineConfig.SubgraphErrorPropagation.AttachServiceName, DefaultErrorExtensionCode: opts.RouterEngineConfig.SubgraphErrorPropagation.DefaultExtensionCode, AllowedSubgraphErrorFields: opts.RouterEngineConfig.SubgraphErrorPropagation.AllowedFields, + AllowAllErrorExtensionFields: opts.RouterEngineConfig.SubgraphErrorPropagation.AllowAllExtensionFields, MaxRecyclableParserSize: opts.RouterEngineConfig.Execution.ResolverMaxRecyclableParserSize, MultipartSubHeartbeatInterval: opts.HeartbeatInterval, MaxSubscriptionFetchTimeout: opts.RouterEngineConfig.Execution.SubscriptionFetchTimeout, @@ -100,7 +101,7 @@ func (b *ExecutorConfigurationBuilder) Build(ctx context.Context, opts *Executor } if opts.ApolloRouterCompatibilityFlags.SubrequestHTTPError.Enabled { - options.ResolvableOptions.ApolloRouterCompatibilitySubrequestHTTPError = true + options.ApolloRouterCompatibilitySubrequestHTTPError = true } switch opts.RouterEngineConfig.SubgraphErrorPropagation.Mode { diff --git a/router/go.mod b/router/go.mod index d618c7b3ca..e3d2df7bef 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.202 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.203 // Do not upgrade, it renames attributes we rely on go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/contrib/propagators/b3 v1.23.0 diff --git a/router/go.sum b/router/go.sum index 16902221b1..f25357ae93 100644 --- a/router/go.sum +++ b/router/go.sum @@ -290,8 +290,8 @@ github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0 github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.202 h1:C0rQNddwMMou4o1iXHhXiOlY6/IUF2yS9Pw3u3hd+ts= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.202/go.mod h1:DaBrBCMgKGd3t7zg7z11jKm+0mVJiesr/IQCRG9qgP0= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.203 h1:qTMYS9EICDCoMY90ILE3eW2/i1VNMhmyl79qpw5v6xc= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.203/go.mod h1:DaBrBCMgKGd3t7zg7z11jKm+0mVJiesr/IQCRG9qgP0= 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/pkg/config/config.go b/router/pkg/config/config.go index 704d48f6ef..923fcac36f 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -724,16 +724,17 @@ const ( ) type SubgraphErrorPropagationConfiguration struct { - Enabled bool `yaml:"enabled" envDefault:"true" env:"SUBGRAPH_ERROR_PROPAGATION_ENABLED"` - PropagateStatusCodes bool `yaml:"propagate_status_codes" envDefault:"false" env:"SUBGRAPH_ERROR_PROPAGATION_STATUS_CODES"` - Mode SubgraphErrorPropagationMode `yaml:"mode" envDefault:"wrapped" env:"SUBGRAPH_ERROR_PROPAGATION_MODE"` - RewritePaths bool `yaml:"rewrite_paths" envDefault:"true" env:"SUBGRAPH_ERROR_PROPAGATION_REWRITE_PATHS"` - OmitLocations bool `yaml:"omit_locations" envDefault:"true" env:"SUBGRAPH_ERROR_PROPAGATION_OMIT_LOCATIONS"` - OmitExtensions bool `yaml:"omit_extensions" envDefault:"false" env:"SUBGRAPH_ERROR_PROPAGATION_OMIT_EXTENSIONS"` - AttachServiceName bool `yaml:"attach_service_name" envDefault:"true" env:"SUBGRAPH_ERROR_PROPAGATION_ATTACH_SERVICE_NAME"` - DefaultExtensionCode string `yaml:"default_extension_code" envDefault:"DOWNSTREAM_SERVICE_ERROR" env:"SUBGRAPH_ERROR_PROPAGATION_DEFAULT_EXTENSION_CODE"` - AllowedExtensionFields []string `yaml:"allowed_extension_fields" envDefault:"code" env:"SUBGRAPH_ERROR_PROPAGATION_ALLOWED_EXTENSION_FIELDS"` - AllowedFields []string `yaml:"allowed_fields" env:"SUBGRAPH_ERROR_PROPAGATION_ALLOWED_FIELDS"` + Enabled bool `yaml:"enabled" envDefault:"true" env:"ENABLED"` + PropagateStatusCodes bool `yaml:"propagate_status_codes" envDefault:"false" env:"STATUS_CODES"` + Mode SubgraphErrorPropagationMode `yaml:"mode" envDefault:"wrapped" env:"MODE"` + RewritePaths bool `yaml:"rewrite_paths" envDefault:"true" env:"REWRITE_PATHS"` + OmitLocations bool `yaml:"omit_locations" envDefault:"true" env:"OMIT_LOCATIONS"` + OmitExtensions bool `yaml:"omit_extensions" envDefault:"false" env:"OMIT_EXTENSIONS"` + AttachServiceName bool `yaml:"attach_service_name" envDefault:"true" env:"ATTACH_SERVICE_NAME"` + DefaultExtensionCode string `yaml:"default_extension_code" envDefault:"DOWNSTREAM_SERVICE_ERROR" env:"DEFAULT_EXTENSION_CODE"` + AllowAllExtensionFields bool `yaml:"allow_all_extension_fields" envDefault:"false" env:"ALLOW_ALL_EXTENSION_FIELDS"` + AllowedExtensionFields []string `yaml:"allowed_extension_fields" envDefault:"code" env:"ALLOWED_EXTENSION_FIELDS"` + AllowedFields []string `yaml:"allowed_fields" env:"ALLOWED_FIELDS"` } type StorageProviders struct { @@ -1009,7 +1010,7 @@ type Config struct { WebSocket WebSocketConfiguration `yaml:"websocket,omitempty"` - SubgraphErrorPropagation SubgraphErrorPropagationConfiguration `yaml:"subgraph_error_propagation"` + SubgraphErrorPropagation SubgraphErrorPropagationConfiguration `yaml:"subgraph_error_propagation" envPrefix:"SUBGRAPH_ERROR_PROPAGATION_"` StorageProviders StorageProviders `yaml:"storage_providers"` ExecutionConfig ExecutionConfig `yaml:"execution_config"` diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index f8b66502fc..477be33e9a 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -2706,6 +2706,11 @@ "default": ["code"], "description": "The allowed extension fields. The allowed extension fields are used to specify which fields of the Subgraph errors are allowed to be propagated to the client." }, + "allow_all_extension_fields": { + "type": "boolean", + "default": false, + "description": "Allow all extension fields from Subgraph errors to be propagated to the client. If the value is true (default: false), all extension fields from Subgraph errors will be propagated, overriding the allowed_extension_fields configuration." + }, "omit_locations": { "type": "boolean", "default": true, diff --git a/router/pkg/config/fixtures/full.yaml b/router/pkg/config/fixtures/full.yaml index 7d000471b8..c8726845b8 100644 --- a/router/pkg/config/fixtures/full.yaml +++ b/router/pkg/config/fixtures/full.yaml @@ -314,7 +314,7 @@ events: redis: - id: my-redis urls: - - "redis://localhost:6379/11" + - 'redis://localhost:6379/11' cluster_enabled: true engine: @@ -445,6 +445,19 @@ automatic_persisted_queries: provider_id: redis object_prefix: 'cosmo_apq' +subgraph_error_propagation: + mode: pass-through + rewrite_paths: true + attach_service_name: true + default_extension_code: DOWNSTREAM_SERVICE_ERROR + omit_locations: true + omit_extensions: true + propagate_status_codes: false + allowed_extension_fields: + - 'field1' + - 'field2' + allow_all_extension_fields: true + execution_config: storage: provider_id: s3 diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index 82189fb0b0..0e11cad37c 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -406,6 +406,7 @@ "OmitExtensions": false, "AttachServiceName": true, "DefaultExtensionCode": "DOWNSTREAM_SERVICE_ERROR", + "AllowAllExtensionFields": false, "AllowedExtensionFields": [ "code" ], diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index 2db2e1cdee..5be309fc5d 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -747,14 +747,16 @@ "SubgraphErrorPropagation": { "Enabled": true, "PropagateStatusCodes": false, - "Mode": "wrapped", + "Mode": "pass-through", "RewritePaths": true, "OmitLocations": true, - "OmitExtensions": false, + "OmitExtensions": true, "AttachServiceName": true, "DefaultExtensionCode": "DOWNSTREAM_SERVICE_ERROR", + "AllowAllExtensionFields": true, "AllowedExtensionFields": [ - "code" + "field1", + "field2" ], "AllowedFields": null },