Skip to content

Commit e15b640

Browse files
authored
fix: handle any invalid locations gracefully (#2427)
1 parent cfffbce commit e15b640

File tree

5 files changed

+151
-6
lines changed

5 files changed

+151
-6
lines changed

router-tests/error_handling_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,4 +1492,149 @@ func TestErrorPropagation(t *testing.T) {
14921492
require.Equal(t, expected, resp.Body)
14931493
})
14941494
})
1495+
1496+
}
1497+
1498+
func TestErrorLocations(t *testing.T) {
1499+
t.Parallel()
1500+
1501+
t.Run("Handle invalid locations", func(t *testing.T) {
1502+
t.Parallel()
1503+
1504+
tests := []struct {
1505+
name string
1506+
subgraphErrorsInput string
1507+
expectedWrappedResponse string
1508+
expectedPassthroughResponse string
1509+
}{
1510+
{
1511+
name: "all locations invalid - removes locations field",
1512+
subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":-1,"column":1}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1513+
expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`,
1514+
expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`,
1515+
},
1516+
{
1517+
name: "mixed valid and invalid locations - keeps only valid",
1518+
subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":0,"column":10},{"line":3,"column":-2},{"line":4,"column":15}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1519+
expectedWrappedResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":4,"column":15}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1520+
expectedPassthroughResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":4,"column":15}],"extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`,
1521+
},
1522+
{
1523+
name: "location with missing line field - removes that location",
1524+
subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"column":10}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1525+
expectedWrappedResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1526+
expectedPassthroughResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5}],"extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`,
1527+
},
1528+
{
1529+
name: "all locations missing fields - removes locations field",
1530+
subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1},{"column":5}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1531+
expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`,
1532+
expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`,
1533+
},
1534+
{
1535+
name: "location with invalid type - removes that location",
1536+
subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":"invalid","column":10}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1537+
expectedWrappedResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1538+
expectedPassthroughResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5}],"extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`,
1539+
},
1540+
{
1541+
name: "all locations with invalid types - removes locations field",
1542+
subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":"invalid","column":5},{"line":2,"column":"invalid"}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1543+
expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`,
1544+
expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`,
1545+
},
1546+
{
1547+
name: "locations is not an array - removes locations field",
1548+
subgraphErrorsInput: `[{"message":"Unauthorized","locations":"invalid","extensions":{"code":"UNAUTHORIZED"}}]`,
1549+
expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`,
1550+
expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`,
1551+
},
1552+
{
1553+
name: "multiple errors with different location scenarios",
1554+
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"}}]`,
1555+
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"}}]`,
1556+
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}}]`,
1557+
},
1558+
{
1559+
name: "valid locations - unchanged",
1560+
subgraphErrorsInput: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":2,"column":10}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1561+
expectedWrappedResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":2,"column":10}],"extensions":{"code":"UNAUTHORIZED"}}]`,
1562+
expectedPassthroughResponse: `[{"message":"Unauthorized","locations":[{"line":1,"column":5},{"line":2,"column":10}],"extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`,
1563+
},
1564+
{
1565+
name: "no locations field - unchanged",
1566+
subgraphErrorsInput: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`,
1567+
expectedWrappedResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED"}}]`,
1568+
expectedPassthroughResponse: `[{"message":"Unauthorized","extensions":{"code":"UNAUTHORIZED","statusCode":200}}]`,
1569+
},
1570+
}
1571+
1572+
for _, tt := range tests {
1573+
t.Run(tt.name, func(t *testing.T) {
1574+
t.Parallel()
1575+
1576+
t.Run("wrapped mode", func(t *testing.T) {
1577+
t.Parallel()
1578+
testenv.Run(t, &testenv.Config{
1579+
ModifySubgraphErrorPropagation: func(cfg *config.SubgraphErrorPropagationConfiguration) {
1580+
cfg.Enabled = true
1581+
cfg.Mode = config.SubgraphErrorPropagationModeWrapped
1582+
cfg.OmitLocations = false
1583+
},
1584+
Subgraphs: testenv.SubgraphsConfig{
1585+
Employees: testenv.SubgraphConfig{
1586+
Middleware: func(handler http.Handler) http.Handler {
1587+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1588+
w.WriteHeader(http.StatusOK)
1589+
subgraphResponse := `{"errors":` + tt.subgraphErrorsInput + `}`
1590+
if _, err := w.Write([]byte(subgraphResponse)); err != nil {
1591+
http.Error(w, err.Error(), http.StatusInternalServerError)
1592+
}
1593+
})
1594+
},
1595+
},
1596+
},
1597+
}, func(t *testing.T, xEnv *testenv.Environment) {
1598+
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
1599+
Query: `{ employees { id details { forename surname } notes } }`,
1600+
})
1601+
1602+
expectedBody := `{"errors":[{"message":"Failed to fetch from Subgraph 'employees'.","extensions":{"errors":` + tt.expectedWrappedResponse + `,"statusCode":200}}],"data":{"employees":null}}`
1603+
require.JSONEq(t, expectedBody, res.Body)
1604+
})
1605+
})
1606+
1607+
t.Run("passthrough mode", func(t *testing.T) {
1608+
t.Parallel()
1609+
testenv.Run(t, &testenv.Config{
1610+
ModifySubgraphErrorPropagation: func(cfg *config.SubgraphErrorPropagationConfiguration) {
1611+
cfg.Enabled = true
1612+
cfg.Mode = config.SubgraphErrorPropagationModePassthrough
1613+
cfg.OmitLocations = false
1614+
},
1615+
Subgraphs: testenv.SubgraphsConfig{
1616+
Employees: testenv.SubgraphConfig{
1617+
Middleware: func(handler http.Handler) http.Handler {
1618+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1619+
w.WriteHeader(http.StatusOK)
1620+
subgraphResponse := `{"errors":` + tt.subgraphErrorsInput + `}`
1621+
if _, err := w.Write([]byte(subgraphResponse)); err != nil {
1622+
http.Error(w, err.Error(), http.StatusInternalServerError)
1623+
}
1624+
})
1625+
},
1626+
},
1627+
},
1628+
}, func(t *testing.T, xEnv *testenv.Environment) {
1629+
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
1630+
Query: `{ employees { id details { forename surname } notes } }`,
1631+
})
1632+
1633+
expectedBody := `{"errors":` + tt.expectedPassthroughResponse + `,"data":{"employees":null}}`
1634+
require.JSONEq(t, expectedBody, res.Body)
1635+
})
1636+
})
1637+
})
1638+
}
1639+
})
14951640
}

router-tests/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ require (
2626
github.com/wundergraph/cosmo/demo/pkg/subgraphs/projects v0.0.0-20250715110703-10f2e5f9c79e
2727
github.com/wundergraph/cosmo/router v0.0.0-20251125205644-175f80c4e6d9
2828
github.com/wundergraph/cosmo/router-plugin v0.0.0-20250808194725-de123ba1c65e
29-
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.241
29+
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242
3030
go.opentelemetry.io/otel v1.36.0
3131
go.opentelemetry.io/otel/sdk v1.36.0
3232
go.opentelemetry.io/otel/sdk/metric v1.36.0

router-tests/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/
352352
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
353353
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk=
354354
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE=
355-
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.241 h1:ch/8hfDaw4oz1Cx3Wb+OUl4qiAo17OdGhYMdRYnX8Is=
356-
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.241/go.mod h1:mX25ASEQiKamxaFSK6NZihh0oDCigIuzro30up4mFH4=
355+
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242 h1:0ieQmRxYz0nbJEbaaA4Cx2RPcxlomhQ8KI31uuevWx0=
356+
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242/go.mod h1:mX25ASEQiKamxaFSK6NZihh0oDCigIuzro30up4mFH4=
357357
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
358358
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
359359
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=

router/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ require (
3131
github.com/tidwall/gjson v1.18.0
3232
github.com/tidwall/sjson v1.2.5
3333
github.com/twmb/franz-go v1.16.1
34-
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.241
34+
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242
3535
// Do not upgrade, it renames attributes we rely on
3636
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0
3737
go.opentelemetry.io/contrib/propagators/b3 v1.23.0

router/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/
322322
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
323323
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk=
324324
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE=
325-
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.241 h1:ch/8hfDaw4oz1Cx3Wb+OUl4qiAo17OdGhYMdRYnX8Is=
326-
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.241/go.mod h1:mX25ASEQiKamxaFSK6NZihh0oDCigIuzro30up4mFH4=
325+
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242 h1:0ieQmRxYz0nbJEbaaA4Cx2RPcxlomhQ8KI31uuevWx0=
326+
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242/go.mod h1:mX25ASEQiKamxaFSK6NZihh0oDCigIuzro30up4mFH4=
327327
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
328328
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
329329
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=

0 commit comments

Comments
 (0)