diff --git a/router-tests/error_handling_test.go b/router-tests/error_handling_test.go index 394b712492..816f7ff840 100644 --- a/router-tests/error_handling_test.go +++ b/router-tests/error_handling_test.go @@ -1492,4 +1492,149 @@ func TestErrorPropagation(t *testing.T) { require.Equal(t, expected, resp.Body) }) }) + +} + +func TestErrorLocations(t *testing.T) { + t.Parallel() + + t.Run("Handle invalid locations", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + subgraphErrorsInput string + expectedWrappedResponse string + expectedPassthroughResponse string + }{ + { + name: "all locations invalid - removes locations field", + subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":-1,"column":1}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`, + expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`, + }, + { + name: "mixed valid and invalid locations - keeps only valid", + subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":0,"column":10},{"line":3,"column":-2},{"line":4,"column":15}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedWrappedResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":4,"column":15}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedPassthroughResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":4,"column":15}],"extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`, + }, + { + name: "location with missing line field - removes that location", + subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"column":10}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedWrappedResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedPassthroughResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5}],"extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`, + }, + { + name: "all locations missing fields - removes locations field", + subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1},{"column":5}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`, + expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`, + }, + { + name: "location with invalid type - removes that location", + subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":"invalid","column":10}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedWrappedResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedPassthroughResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5}],"extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`, + }, + { + name: "all locations with invalid types - removes locations field", + subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":"invalid","column":5},{"line":2,"column":"invalid"}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`, + expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`, + }, + { + name: "locations is not an array - removes locations field", + subgraphErrorsInput: `[{"message":"Unauthorized","locations":"invalid","extensions":{"code":"UNAUTHORIZED"}}]`, + expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`, + expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`, + }, + { + name: "multiple errors with different location scenarios", + subgraphErrorsInput: `[{"message":"Error 1","locations":[{"line":1,"column":5}],"extensions":{"code":"ERR1"}},{"message":"Error 2","locations":[{"line":0,"column":0}],"extensions":{"code":"ERR2"}},{"message":"Error 3","locations":[{"line":3,"column":10},{"line":-1,"column":5}],"extensions":{"code":"ERR3"}}]`, + expectedWrappedResponse: `[{"message":"Error 1","locations":[{"line":1,"column":5}],"extensions":{"code":"ERR1"}},{"message":"Error 2","extensions":{"code":"ERR2"}},{"message":"Error 3","locations":[{"line":3,"column":10}],"extensions":{"code":"ERR3"}}]`, + expectedPassthroughResponse: `[{"message":"Error 1","locations":[{"line":1,"column":5}],"extensions":{"code":"ERR1","statusCode":200}},{"message":"Error 2","extensions":{"code":"ERR2","statusCode":200}},{"message":"Error 3","locations":[{"line":3,"column":10}],"extensions":{"code":"ERR3","statusCode":200}}]`, + }, + { + name: "valid locations - unchanged", + subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":2,"column":10}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedWrappedResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":2,"column":10}],"extensions":{"code":"UNAUTHORIZED"}}]`, + expectedPassthroughResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":2,"column":10}],"extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`, + }, + { + name: "no locations field - unchanged", + subgraphErrorsInput: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`, + expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`, + expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + t.Run("wrapped mode", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + ModifySubgraphErrorPropagation: func(cfg *config.SubgraphErrorPropagationConfiguration) { + cfg.Enabled = true + cfg.Mode = config.SubgraphErrorPropagationModeWrapped + cfg.OmitLocations = false + }, + Subgraphs: testenv.SubgraphsConfig{ + Employees: testenv.SubgraphConfig{ + Middleware: func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + subgraphResponse := `{"errors":` + tt.subgraphErrorsInput + `}` + if _, err := w.Write([]byte(subgraphResponse)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + }, + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `{ employees { id details { forename surname } notes } }`, + }) + + expectedBody := `{"errors":[{"message":"Failed to fetch from Subgraph 'employees'.","extensions":{"errors":` + tt.expectedWrappedResponse + `,"statusCode":200}}],"data":{"employees":null}}` + require.JSONEq(t, expectedBody, res.Body) + }) + }) + + t.Run("passthrough mode", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + ModifySubgraphErrorPropagation: func(cfg *config.SubgraphErrorPropagationConfiguration) { + cfg.Enabled = true + cfg.Mode = config.SubgraphErrorPropagationModePassthrough + cfg.OmitLocations = false + }, + Subgraphs: testenv.SubgraphsConfig{ + Employees: testenv.SubgraphConfig{ + Middleware: func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + subgraphResponse := `{"errors":` + tt.subgraphErrorsInput + `}` + if _, err := w.Write([]byte(subgraphResponse)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + }, + }, + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ + Query: `{ employees { id details { forename surname } notes } }`, + }) + + expectedBody := `{"errors":` + tt.expectedPassthroughResponse + `,"data":{"employees":null}}` + require.JSONEq(t, expectedBody, res.Body) + }) + }) + }) + } + }) } diff --git a/router-tests/go.mod b/router-tests/go.mod index efb12b0924..c9a3abaf01 100644 --- a/router-tests/go.mod +++ b/router-tests/go.mod @@ -26,7 +26,7 @@ require ( github.com/wundergraph/cosmo/demo/pkg/subgraphs/projects v0.0.0-20250715110703-10f2e5f9c79e github.com/wundergraph/cosmo/router v0.0.0-20251125205644-175f80c4e6d9 github.com/wundergraph/cosmo/router-plugin v0.0.0-20250808194725-de123ba1c65e - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.241 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242 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 e1495ba16e..6370091021 100644 --- a/router-tests/go.sum +++ b/router-tests/go.sum @@ -352,8 +352,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 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.241 h1:ch/8hfDaw4oz1Cx3Wb+OUl4qiAo17OdGhYMdRYnX8Is= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.241/go.mod h1:mX25ASEQiKamxaFSK6NZihh0oDCigIuzro30up4mFH4= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242 h1:0ieQmRxYz0nbJEbaaA4Cx2RPcxlomhQ8KI31uuevWx0= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242/go.mod h1:mX25ASEQiKamxaFSK6NZihh0oDCigIuzro30up4mFH4= 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/go.mod b/router/go.mod index 4e5bbfd302..bde71925bf 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.241 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242 // 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 c018f10ca5..bf848f1712 100644 --- a/router/go.sum +++ b/router/go.sum @@ -322,8 +322,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 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.241 h1:ch/8hfDaw4oz1Cx3Wb+OUl4qiAo17OdGhYMdRYnX8Is= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.241/go.mod h1:mX25ASEQiKamxaFSK6NZihh0oDCigIuzro30up4mFH4= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242 h1:0ieQmRxYz0nbJEbaaA4Cx2RPcxlomhQ8KI31uuevWx0= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242/go.mod h1:mX25ASEQiKamxaFSK6NZihh0oDCigIuzro30up4mFH4= 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=