From d4d727e1c98f92eaa2103ca2356537e3a63eeff2 Mon Sep 17 00:00:00 2001 From: Aenimus <47415099+Aenimus@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:02:46 +0000 Subject: [PATCH 1/4] feat: extend apollo compatible error support (#1311) --- aws-lambda-router/go.mod | 4 +- aws-lambda-router/go.sum | 8 +- demo/go.mod | 4 +- demo/go.sum | 6 +- go.work | 4 + go.work.sum | 2 + router-tests/apollo_compatibility_test.go | 123 ++++++++++++++---- router-tests/feature_flags_test.go | 8 +- router-tests/go.mod | 6 +- router-tests/go.sum | 12 +- router-tests/integration_test.go | 2 +- router-tests/structured_logging_test.go | 6 +- router-tests/telemetry_test.go | 29 +++-- router/core/errors.go | 84 +++++++----- router/core/executor.go | 6 + router/core/graph_server.go | 2 + router/core/graphql_prehandler.go | 3 + router/core/http_graphql_error.go | 11 +- router/core/operation_processor.go | 35 ++++- router/core/router.go | 2 + router/go.mod | 2 +- router/go.sum | 4 +- router/pkg/config/config.go | 18 ++- .../pkg/config/testdata/config_defaults.json | 6 + router/pkg/config/testdata/config_full.json | 6 + 25 files changed, 280 insertions(+), 113 deletions(-) diff --git a/aws-lambda-router/go.mod b/aws-lambda-router/go.mod index a8aee2ee16..3f87a1388d 100644 --- a/aws-lambda-router/go.mod +++ b/aws-lambda-router/go.mod @@ -4,7 +4,7 @@ require ( github.com/akrylysov/algnhsa v1.1.0 github.com/aws/aws-lambda-go v1.43.0 github.com/stretchr/testify v1.9.0 - github.com/wundergraph/cosmo/router v0.0.0-20241027092036-e74bdb968e8a + github.com/wundergraph/cosmo/router v0.0.0-20241028212443-646650d431c5 go.uber.org/zap v1.27.0 ) @@ -93,7 +93,7 @@ require ( github.com/twmb/franz-go v1.16.1 // indirect github.com/twmb/franz-go/pkg/kmsg v1.7.0 // indirect github.com/wundergraph/astjson v0.0.0-20240910140849-bb15f94bd362 // indirect - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113 // indirect + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.23.0 // indirect diff --git a/aws-lambda-router/go.sum b/aws-lambda-router/go.sum index 954fea8d98..35407c4f7e 100644 --- a/aws-lambda-router/go.sum +++ b/aws-lambda-router/go.sum @@ -250,10 +250,10 @@ 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-20240910140849-bb15f94bd362 h1:MxNSJqQFJyhKwU4xPj6diIRLm+oY1wNbAZW0jJpikBE= github.com/wundergraph/astjson v0.0.0-20240910140849-bb15f94bd362/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/cosmo/router v0.0.0-20241027092036-e74bdb968e8a h1:Y8eZOHfqLU4nbkzlGEfic54wL0zjwO8ZmSy59omADC0= -github.com/wundergraph/cosmo/router v0.0.0-20241027092036-e74bdb968e8a/go.mod h1:BpOyKj34vLTYK694X2qjQLuc6LcHKKzMIl/9Qhytobg= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113 h1:lo6ZgsLNJF4Qc+TCH4h0KPc48ew5GS6i1rB/iCjRfBg= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113/go.mod h1:2FThDhi9IFpRv/l/L4BQJM2+Az8TYdq7tFycTPGOuFY= +github.com/wundergraph/cosmo/router v0.0.0-20241028212443-646650d431c5 h1:K10t90fNSqBpfYvboMMWaRLp2gELe2s3OcFXWoR4V3s= +github.com/wundergraph/cosmo/router v0.0.0-20241028212443-646650d431c5/go.mod h1:3wz3decKAtmKV763fUQcyLm705vq/hkvgQTeYPqBjkc= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115 h1:D45Z2XpuQjHtKEDSNX41r1aLkQGS2oB8I43vcDTSmEw= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115/go.mod h1:2FThDhi9IFpRv/l/L4BQJM2+Az8TYdq7tFycTPGOuFY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= diff --git a/demo/go.mod b/demo/go.mod index 204dd76eeb..60f5e03739 100644 --- a/demo/go.mod +++ b/demo/go.mod @@ -13,9 +13,9 @@ require ( github.com/rs/cors v1.11.0 github.com/vektah/gqlparser/v2 v2.5.16 github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d - github.com/wundergraph/cosmo/router v0.0.0-20241027092036-e74bdb968e8a + github.com/wundergraph/cosmo/router v0.0.0-20241029112307-e101eaccac90 github.com/wundergraph/cosmo/router-tests v0.0.0-20241024215101-0c757faf23de - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.23.1 diff --git a/demo/go.sum b/demo/go.sum index afa8cd5fc6..a1be3a080d 100644 --- a/demo/go.sum +++ b/demo/go.sum @@ -299,11 +299,11 @@ github.com/wundergraph/astjson v0.0.0-20240910140849-bb15f94bd362 h1:MxNSJqQFJyh github.com/wundergraph/astjson v0.0.0-20240910140849-bb15f94bd362/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d h1:NEUrhuqOaTO1dpW8pz2tu6dKbQAqFvgiF/m4NXdzZm0= github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d/go.mod h1:9I3gPMAlAY+m1/cFL20iN7XHTyuZd3VT5ijccdU/FsI= -github.com/wundergraph/cosmo/router v0.0.0-20241027092036-e74bdb968e8a h1:Y8eZOHfqLU4nbkzlGEfic54wL0zjwO8ZmSy59omADC0= -github.com/wundergraph/cosmo/router v0.0.0-20241027092036-e74bdb968e8a/go.mod h1:BpOyKj34vLTYK694X2qjQLuc6LcHKKzMIl/9Qhytobg= +github.com/wundergraph/cosmo/router v0.0.0-20241029112307-e101eaccac90 h1:+P+uBq3nCrK11d1acHiFJ2UXGUAPsqFAzzT3TsiJdlI= +github.com/wundergraph/cosmo/router v0.0.0-20241029112307-e101eaccac90/go.mod h1:PeJdcguyQm4RZgdQp6WSEdvPD/+2tDmDkIAVlSXN4L4= github.com/wundergraph/cosmo/router-tests v0.0.0-20241024215101-0c757faf23de h1:kEiNUGYSxHaT1I5GKre0GqHFCHVabpC/N33Chfue/rs= github.com/wundergraph/cosmo/router-tests v0.0.0-20241024215101-0c757faf23de/go.mod h1:7WvZF+cOkfV4GaXka1qZfCqf/9GmOcksbPoMmJXtja4= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113 h1:lo6ZgsLNJF4Qc+TCH4h0KPc48ew5GS6i1rB/iCjRfBg= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115 h1:D45Z2XpuQjHtKEDSNX41r1aLkQGS2oB8I43vcDTSmEw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/go.work b/go.work index ca96c8bcae..a5a9bf4698 100644 --- a/go.work +++ b/go.work @@ -11,6 +11,10 @@ use ( ./router-tests ) +// Two dirs up // replace github.com/wundergraph/graphql-go-tools/v2 => ../../graphql-go-tools/v2 +// One dir up +//replace github.com/wundergraph/graphql-go-tools/v2 => ../graphql-go-tools/v2 + //replace github.com/wundergraph/astjson => ../astjson diff --git a/go.work.sum b/go.work.sum index 8a302d73d9..4b6ac065d9 100644 --- a/go.work.sum +++ b/go.work.sum @@ -444,6 +444,8 @@ github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.90/go.mod h1:zkPVYJu1iQd0y1 github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.95/go.mod h1:zkPVYJu1iQd0y1fBNj+oXe9uMI/33TSoiXEsKSAESZY= github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.102 h1:UUT6mLcUH1sGepg+GFZr6TfC+mkZWF3YLdnQruBvIZA= github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.102/go.mod h1:zkPVYJu1iQd0y1fBNj+oXe9uMI/33TSoiXEsKSAESZY= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115 h1:D45Z2XpuQjHtKEDSNX41r1aLkQGS2oB8I43vcDTSmEw= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115/go.mod h1:2FThDhi9IFpRv/l/L4BQJM2+Az8TYdq7tFycTPGOuFY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= diff --git a/router-tests/apollo_compatibility_test.go b/router-tests/apollo_compatibility_test.go index 3e6b48cdb4..bad2def1c0 100644 --- a/router-tests/apollo_compatibility_test.go +++ b/router-tests/apollo_compatibility_test.go @@ -2,6 +2,7 @@ package integration import ( "encoding/json" + "github.com/stretchr/testify/assert" "net/http" "testing" @@ -37,8 +38,8 @@ func TestApolloCompatibility(t *testing.T) { Variables: json.RawMessage(`{"criteria":{"nationality":"GERMAN"}}`), }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Invalid __typename found for object at array element of type Employee at index 0.","path":["findEmployees",0],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Invalid __typename found for object at array element of type Employee at index 0.","path":["findEmployees",0],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) }) }) t.Run("enable value completion", func(t *testing.T) { @@ -65,8 +66,8 @@ func TestApolloCompatibility(t *testing.T) { Variables: json.RawMessage(`{"criteria":{"nationality":"GERMAN"}}`), }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Invalid __typename found for object at array element of type Employee at index 0.","path":["findEmployees",0],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Invalid __typename found for object at array element of type Employee at index 0.","path":["findEmployees",0],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) }) }) t.Run("float compaction off", func(t *testing.T) { @@ -86,8 +87,8 @@ func TestApolloCompatibility(t *testing.T) { Variables: json.RawMessage(`{"arg":1.0}`), }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":{"floatField":1.0}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":{"floatField":1.0}}`, res.Body) }) }) t.Run("should not truncate - off", func(t *testing.T) { @@ -107,8 +108,8 @@ func TestApolloCompatibility(t *testing.T) { Variables: json.RawMessage(`{"arg":1.1}`), }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":{"floatField":1.1}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":{"floatField":1.1}}`, res.Body) }) }) t.Run("should not truncate - on", func(t *testing.T) { @@ -135,8 +136,8 @@ func TestApolloCompatibility(t *testing.T) { Variables: json.RawMessage(`{"arg":1.1}`), }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":{"floatField":1.1}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":{"floatField":1.1}}`, res.Body) }) }) t.Run("float compaction on", func(t *testing.T) { @@ -163,8 +164,8 @@ func TestApolloCompatibility(t *testing.T) { Variables: json.RawMessage(`{"arg":1}`), }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":{"floatField":1}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":{"floatField":1}}`, res.Body) }) }) t.Run("float compaction global", func(t *testing.T) { @@ -189,8 +190,8 @@ func TestApolloCompatibility(t *testing.T) { Variables: json.RawMessage(`{"arg":1}`), }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":{"floatField":1}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":{"floatField":1}}`, res.Body) }) }) t.Run("nullable array item with non-nullable array item field", func(t *testing.T) { @@ -216,8 +217,8 @@ func TestApolloCompatibility(t *testing.T) { Query: `query {employees{id}}`, }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":{"employees":[null]},"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Employee.id.","path":["employees",0,"id"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":{"employees":[null]},"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Employee.id.","path":["employees",0,"id"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) }) }) t.Run("non-nullable array item", func(t *testing.T) { @@ -243,8 +244,8 @@ func TestApolloCompatibility(t *testing.T) { Query: `query {products{... on Consultancy{upc}}}`, }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable array element of type Products at index 0.","path":["products",0],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable array element of type Products at index 0.","path":["products",0],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) }) }) t.Run("non-nullable array item with non-nullable array item field", func(t *testing.T) { @@ -270,8 +271,8 @@ func TestApolloCompatibility(t *testing.T) { Query: `query {products{... on Consultancy{upc}}}`, }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Products.upc.","path":["products",0,"upc"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Products.upc.","path":["products",0,"upc"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) }) }) t.Run("simple fetch with suppress fetch errors enabled", func(t *testing.T) { @@ -300,8 +301,8 @@ func TestApolloCompatibility(t *testing.T) { Query: `query {products{... on Consultancy{upc}}}`, }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Query.products.","path":["products"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Query.products.","path":["products"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) }) }) t.Run("simple fetch with suppress fetch errors disabled", func(t *testing.T) { @@ -330,8 +331,8 @@ func TestApolloCompatibility(t *testing.T) { Query: `query {products{... on Consultancy{upc}}}`, }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"errors":[{"message":"Failed to fetch from Subgraph 'employees', Reason: no data or errors in response.","extensions":{"statusCode":200}}],"data":null}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"errors":[{"message":"Failed to fetch from Subgraph 'employees', Reason: no data or errors in response.","extensions":{"statusCode":200}}],"data":null}`, res.Body) }) }) t.Run("should suppress errors when enable all is true", func(t *testing.T) { @@ -355,8 +356,78 @@ func TestApolloCompatibility(t *testing.T) { Query: `query {products{... on Consultancy{upc}}}`, }) require.NoError(t, err) - require.Equal(t, http.StatusOK, res.Response.StatusCode) - require.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Query.products.","path":["products"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"data":null,"extensions":{"valueCompletion":[{"message":"Cannot return null for non-nullable field Query.products.","path":["products"],"extensions":{"code":"INVALID_GRAPHQL"}}]}}`, res.Body) + }) + }) + t.Run("enable replace undefined operation field error", func(t *testing.T) { + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithApolloCompatibilityFlagsConfig(config.ApolloCompatibilityFlags{ + ReplaceUndefinedOpFieldErrors: config.ApolloCompatibilityReplaceUndefinedOpFieldErrors{ + Enabled: true, + }, + }), + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `query {employees{nonExistentField {id}}}`, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.Response.StatusCode) + assert.Equal(t, `{"errors":[{"message":"Cannot query \"nonExistentField\" on type \"Employee\".","extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}`, res.Body) + }) + }) + t.Run("enable all: replace undefined operation field error", func(t *testing.T) { + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithApolloCompatibilityFlagsConfig(config.ApolloCompatibilityFlags{ + EnableAll: true, + }), + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `query {employees{nonExistentField}}`, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.Response.StatusCode) + assert.Equal(t, `{"errors":[{"message":"Cannot query \"nonExistentField\" on type \"Employee\".","extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}`, res.Body) + }) + }) + t.Run("enable replace invalid variable error", func(t *testing.T) { + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithApolloCompatibilityFlagsConfig(config.ApolloCompatibilityFlags{ + ReplaceInvalidVarErrors: config.ApolloCompatibilityReplaceInvalidVarErrors{ + Enabled: true, + }, + }), + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `query FloatQuery($arg: Float) { floatField(arg: $arg) }`, + Variables: json.RawMessage(`{"arg":"INVALID"}`), + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"errors":[{"message":"Variable \"$arg\" got invalid value \"INVALID\"; Float cannot represent non numeric value: \"INVALID\"","extensions":{"code":"BAD_USER_INPUT"}}]}`, res.Body) + }) + }) + t.Run("enable all: replace invalid variable error", func(t *testing.T) { + testenv.Run(t, &testenv.Config{ + RouterOptions: []core.Option{ + core.WithApolloCompatibilityFlagsConfig(config.ApolloCompatibilityFlags{ + EnableAll: true, + }), + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res, err := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `query FloatQuery($arg: Float) { floatField(arg: $arg) }`, + Variables: json.RawMessage(`{"arg":"INVALID"}`), + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.Response.StatusCode) + assert.Equal(t, `{"errors":[{"message":"Variable \"$arg\" got invalid value \"INVALID\"; Float cannot represent non numeric value: \"INVALID\"","extensions":{"code":"BAD_USER_INPUT"}}]}`, res.Body) }) }) } diff --git a/router-tests/feature_flags_test.go b/router-tests/feature_flags_test.go index 95416a7686..56a3da4468 100644 --- a/router-tests/feature_flags_test.go +++ b/router-tests/feature_flags_test.go @@ -20,7 +20,7 @@ func TestFeatureFlags(t *testing.T) { Query: `{ employees { id productCount } }`, }) require.Empty(t, res.Response.Header.Get("X-Feature-Flag")) - require.JSONEq(t, `{"errors":[{"message":"field: productCount not defined on type: Employee","path":["query","employees","productCount"]}]}`, res.Body) + require.JSONEq(t, `{"errors":[{"message":"field: productCount not defined on type: Employee","path":["query","employees"]}]}`, res.Body) }) }) @@ -36,7 +36,7 @@ func TestFeatureFlags(t *testing.T) { }, }) require.Empty(t, res.Response.Header.Get("X-Feature-Flag")) - require.JSONEq(t, `{"errors":[{"message":"field: productCount not defined on type: Employee","path":["query","employees","productCount"]}]}`, res.Body) + require.JSONEq(t, `{"errors":[{"message":"field: productCount not defined on type: Employee","path":["query","employees"]}]}`, res.Body) }) }) @@ -56,7 +56,7 @@ func TestFeatureFlags(t *testing.T) { }, }) require.Empty(t, res.Response.Header.Get("X-Feature-Flag")) - require.JSONEq(t, `{"errors":[{"message":"field: productCount not defined on type: Employee","path":["query","employees","productCount"]}]}`, res.Body) + require.JSONEq(t, `{"errors":[{"message":"field: productCount not defined on type: Employee","path":["query","employees"]}]}`, res.Body) }) }) @@ -72,7 +72,7 @@ func TestFeatureFlags(t *testing.T) { }, }) require.Empty(t, res.Response.Header.Get("X-Feature-Flag")) - require.JSONEq(t, `{"errors":[{"message":"field: productCount not defined on type: Employee","path":["query","employees","productCount"]}]}`, res.Body) + require.JSONEq(t, `{"errors":[{"message":"field: productCount not defined on type: Employee","path":["query","employees"]}]}`, res.Body) }) }) diff --git a/router-tests/go.mod b/router-tests/go.mod index e0640f0d13..fca49ae3db 100644 --- a/router-tests/go.mod +++ b/router-tests/go.mod @@ -26,9 +26,9 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/twmb/franz-go v1.16.1 github.com/twmb/franz-go/pkg/kadm v1.11.0 - github.com/wundergraph/cosmo/demo v0.0.0-20241027092036-e74bdb968e8a - github.com/wundergraph/cosmo/router v0.0.0-20241027092036-e74bdb968e8a - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113 + github.com/wundergraph/cosmo/demo v0.0.0-20241029112307-e101eaccac90 + github.com/wundergraph/cosmo/router v0.0.0-20241029112307-e101eaccac90 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115 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 cfe095e543..7db6936503 100644 --- a/router-tests/go.sum +++ b/router-tests/go.sum @@ -350,12 +350,12 @@ 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-20240910140849-bb15f94bd362 h1:MxNSJqQFJyhKwU4xPj6diIRLm+oY1wNbAZW0jJpikBE= github.com/wundergraph/astjson v0.0.0-20240910140849-bb15f94bd362/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/cosmo/demo v0.0.0-20241027092036-e74bdb968e8a h1:rCIakcgv3+qiVkv9MoGgPFVgVNdIoC+mQzuMH43CFkc= -github.com/wundergraph/cosmo/demo v0.0.0-20241027092036-e74bdb968e8a/go.mod h1:Xk+uVt4+sub08cGQSIB9n8XAnYXL2fROjiK4Ycgm25A= -github.com/wundergraph/cosmo/router v0.0.0-20241027092036-e74bdb968e8a h1:Y8eZOHfqLU4nbkzlGEfic54wL0zjwO8ZmSy59omADC0= -github.com/wundergraph/cosmo/router v0.0.0-20241027092036-e74bdb968e8a/go.mod h1:BpOyKj34vLTYK694X2qjQLuc6LcHKKzMIl/9Qhytobg= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113 h1:lo6ZgsLNJF4Qc+TCH4h0KPc48ew5GS6i1rB/iCjRfBg= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113/go.mod h1:2FThDhi9IFpRv/l/L4BQJM2+Az8TYdq7tFycTPGOuFY= +github.com/wundergraph/cosmo/demo v0.0.0-20241029112307-e101eaccac90 h1:1IHBZreNKeh9ZxIGYnFpgog9KLBFb6oj58C4VGWb7Xc= +github.com/wundergraph/cosmo/demo v0.0.0-20241029112307-e101eaccac90/go.mod h1:4vE1qQ6u2JuYCPdfzN8buRweV6SbSQz6E2TE1pCI9Kk= +github.com/wundergraph/cosmo/router v0.0.0-20241029112307-e101eaccac90 h1:+P+uBq3nCrK11d1acHiFJ2UXGUAPsqFAzzT3TsiJdlI= +github.com/wundergraph/cosmo/router v0.0.0-20241029112307-e101eaccac90/go.mod h1:PeJdcguyQm4RZgdQp6WSEdvPD/+2tDmDkIAVlSXN4L4= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115 h1:D45Z2XpuQjHtKEDSNX41r1aLkQGS2oB8I43vcDTSmEw= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115/go.mod h1:2FThDhi9IFpRv/l/L4BQJM2+Az8TYdq7tFycTPGOuFY= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/router-tests/integration_test.go b/router-tests/integration_test.go index 5f88e8440b..32bda31e60 100644 --- a/router-tests/integration_test.go +++ b/router-tests/integration_test.go @@ -584,7 +584,7 @@ func TestIntegrationWithUndefinedField(t *testing.T) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: `{ employees { id notDefined } }`, }) - require.JSONEq(t, `{"errors":[{"message":"field: notDefined not defined on type: Employee","path":["query","employees","notDefined"]}]}`, res.Body) + require.JSONEq(t, `{"errors":[{"message":"field: notDefined not defined on type: Employee","path":["query","employees"]}]}`, res.Body) }) } diff --git a/router-tests/structured_logging_test.go b/router-tests/structured_logging_test.go index d0039e9a78..147808009d 100644 --- a/router-tests/structured_logging_test.go +++ b/router-tests/structured_logging_test.go @@ -517,7 +517,7 @@ func TestAccessLogs(t *testing.T) { }) }) - t.Run("Log as much information possible when operation normalization fails", func(t *testing.T) { + t.Run("Log as much information possible when operation validation fails", func(t *testing.T) { t.Parallel() testenv.Run(t, &testenv.Config{ @@ -597,7 +597,7 @@ func TestAccessLogs(t *testing.T) { Query: `query employees { notExists { id } }`, // Missing closing bracket }) require.NoError(t, err) - require.Equal(t, `{"errors":[{"message":"field: notExists not defined on type: Query","path":["query","notExists"]}]}`, res.Body) + require.Equal(t, `{"errors":[{"message":"field: notExists not defined on type: Query","path":["query"]}]}`, res.Body) logEntries := xEnv.Observer().All() require.Len(t, logEntries, 12) requestLog := xEnv.Observer().FilterMessage("/graphql") @@ -615,6 +615,7 @@ func TestAccessLogs(t *testing.T) { "operation_type": "query", // From context "operation_name": "employees", // From context "error_message": "field: notExists not defined on type: Query", + "operation_hash": "10501571900000980785", } additionalExpectedKeys := []string{ "latency", @@ -624,6 +625,7 @@ func TestAccessLogs(t *testing.T) { "hostname", "parsed_time", "normalized_time", + "validation_time", } checkValues(t, requestContext, expectedValues, additionalExpectedKeys) diff --git a/router-tests/telemetry_test.go b/router-tests/telemetry_test.go index 691d3d46ad..c64c69b6a3 100644 --- a/router-tests/telemetry_test.go +++ b/router-tests/telemetry_test.go @@ -2380,10 +2380,10 @@ func TestTelemetry(t *testing.T) { res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ Query: `query foo { employeesTypeNotExist { id } }`, }) - require.Equal(t, `{"errors":[{"message":"field: employeesTypeNotExist not defined on type: Query","path":["query","employeesTypeNotExist"]}]}`, res.Body) + require.Equal(t, `{"errors":[{"message":"field: employeesTypeNotExist not defined on type: Query","path":["query"]}]}`, res.Body) sn := exporter.GetSpans().Snapshots() - require.Len(t, sn, 4, "expected 4 spans, got %d", len(sn)) + require.Len(t, sn, 5, "expected 4 spans, got %d", len(sn)) require.Equal(t, "Operation - Parse", sn[1].Name()) require.Equal(t, trace.SpanKindInternal, sn[1].SpanKind()) @@ -2394,19 +2394,26 @@ func TestTelemetry(t *testing.T) { require.Equal(t, "Operation - Normalize", sn[2].Name()) require.Equal(t, trace.SpanKindInternal, sn[2].SpanKind()) - require.Equal(t, codes.Error, sn[2].Status().Code) - require.Equal(t, sn[2].Status().Description, "field: employeesTypeNotExist not defined on type: Query") + require.Equal(t, codes.Unset, sn[2].Status().Code) + require.Empty(t, sn[2].Status().Description) - events := sn[2].Events() - require.Len(t, events, 1, "expected 1 event because the GraphQL normalization failed") - require.Equal(t, "exception", events[0].Name) + require.Empty(t, sn[2].Events()) - require.Equal(t, "query foo", sn[3].Name()) - require.Equal(t, trace.SpanKindServer, sn[3].SpanKind()) + require.Equal(t, "Operation - Validate", sn[3].Name()) + require.Equal(t, trace.SpanKindInternal, sn[3].SpanKind()) require.Equal(t, codes.Error, sn[3].Status().Code) - require.Contains(t, sn[3].Status().Description, "field: employeesTypeNotExist not defined on type: Query") + require.Equal(t, sn[3].Status().Description, "field: employeesTypeNotExist not defined on type: Query") + + events := sn[3].Events() + require.Len(t, events, 1, "expected 1 event because GraphQL validation failed") + require.Equal(t, "exception", events[0].Name) + + require.Equal(t, "query foo", sn[4].Name()) + require.Equal(t, trace.SpanKindServer, sn[4].SpanKind()) + require.Equal(t, codes.Error, sn[4].Status().Code) + require.Contains(t, sn[4].Status().Description, "field: employeesTypeNotExist not defined on type: Query") - events = sn[3].Events() + events = sn[4].Events() require.Len(t, events, 1, "expected 1 event because the GraphQL request failed") require.Equal(t, "exception", events[0].Name) }) diff --git a/router/core/errors.go b/router/core/errors.go index c79c5a3cb5..3b79246eeb 100644 --- a/router/core/errors.go +++ b/router/core/errors.go @@ -175,41 +175,18 @@ func propagateSubgraphErrors(ctx *resolve.Context) { // writeRequestErrors writes the given request errors to the http.ResponseWriter. // It accepts a graphqlerrors.RequestErrors object and writes it to the response based on the GraphQL spec. func writeRequestErrors(r *http.Request, w http.ResponseWriter, statusCode int, requestErrors graphqlerrors.RequestErrors, requestLogger *zap.Logger) { - if requestErrors != nil { - wgRequestParams := NewWgRequestParams(r) - if wgRequestParams.UseSse { - setSubscriptionHeaders(wgRequestParams, r, w) - - if statusCode != 0 { - w.WriteHeader(statusCode) - } - _, err := w.Write([]byte("event: next\ndata: ")) - if err != nil { - if requestLogger != nil { - if rErrors.IsBrokenPipe(err) { - requestLogger.Warn("Broken pipe, error writing response", zap.Error(err)) - return - } - requestLogger.Error("Error writing response", zap.Error(err)) - } - return - } - } else if wgRequestParams.UseMultipart { - // Handle multipart error response - if err := writeMultipartError(w, requestErrors, requestLogger); err != nil { - if requestLogger != nil { - requestLogger.Error("error writing multipart response", zap.Error(err)) - } - } - return - } + if requestErrors == nil { + return + } + wgRequestParams := NewWgRequestParams(r) + if wgRequestParams.UseSse { + setSubscriptionHeaders(wgRequestParams, r, w) - // Set header before writing status code - w.Header().Set("Content-Type", "application/json") if statusCode != 0 { w.WriteHeader(statusCode) } - if _, err := requestErrors.WriteResponse(w); err != nil { + _, err := w.Write([]byte("event: next\ndata: ")) + if err != nil { if requestLogger != nil { if rErrors.IsBrokenPipe(err) { requestLogger.Warn("Broken pipe, error writing response", zap.Error(err)) @@ -217,6 +194,30 @@ func writeRequestErrors(r *http.Request, w http.ResponseWriter, statusCode int, } requestLogger.Error("Error writing response", zap.Error(err)) } + return + } + } else if wgRequestParams.UseMultipart { + // Handle multipart error response + if err := writeMultipartError(w, requestErrors, requestLogger); err != nil { + if requestLogger != nil { + requestLogger.Error("error writing multipart response", zap.Error(err)) + } + } + return + } + + // Set header before writing status code + w.Header().Set("Content-Type", "application/json") + if statusCode != 0 { + w.WriteHeader(statusCode) + } + if _, err := requestErrors.WriteResponse(w); err != nil { + if requestLogger != nil { + if rErrors.IsBrokenPipe(err) { + requestLogger.Warn("Broken pipe, error writing response", zap.Error(err)) + return + } + requestLogger.Error("Error writing response", zap.Error(err)) } } } @@ -256,6 +257,18 @@ func writeMultipartError(w http.ResponseWriter, requestErrors graphqlerrors.Requ return nil } +func requestErrorsFromHttpError(httpErr HttpError) graphqlerrors.RequestErrors { + requestErr := graphqlerrors.RequestError{ + Message: httpErr.Error(), + } + if httpErr.ExtensionCode() != "" { + requestErr.Extensions = &graphqlerrors.Extensions{ + Code: httpErr.ExtensionCode(), + } + } + return graphqlerrors.RequestErrors{requestErr} +} + // writeOperationError writes the given error to the http.ResponseWriter but evaluates the error type first. // It also logs additional information about the error. func writeOperationError(r *http.Request, w http.ResponseWriter, requestLogger *zap.Logger, err error) { @@ -266,20 +279,19 @@ func writeOperationError(r *http.Request, w http.ResponseWriter, requestLogger * var poNotFoundErr *persistedoperation.PersistentOperationNotFoundError switch { case errors.As(err, &httpErr): - writeRequestErrors(r, w, httpErr.StatusCode(), graphqlerrors.RequestErrorsFromError(err), requestLogger) + writeRequestErrors(r, w, httpErr.StatusCode(), requestErrorsFromHttpError(httpErr), requestLogger) case errors.As(err, &poNotFoundErr): writeRequestErrors(r, w, http.StatusBadRequest, graphqlerrors.RequestErrorsFromError(errors.New("persisted Query not found")), requestLogger) case errors.As(err, &reportErr): report := reportErr.Report() logInternalErrorsFromReport(reportErr.Report(), requestLogger) - requestErrors := graphqlerrors.RequestErrorsFromOperationReport(*report) + statusCode, requestErrors := graphqlerrors.RequestErrorsFromOperationReportWithStatusCode(*report) if len(requestErrors) > 0 { - writeRequestErrors(r, w, http.StatusOK, requestErrors, requestLogger) + writeRequestErrors(r, w, statusCode, requestErrors, requestLogger) return } else { - // there was no external errors to return to user, - // so we return an internal server error + // there were no external errors to return to user, so we return an internal server error writeRequestErrors(r, w, http.StatusInternalServerError, graphqlerrors.RequestErrorsFromError(errInternalServer), requestLogger) } default: diff --git a/router/core/executor.go b/router/core/executor.go index f576ebd60c..42a2b79e23 100644 --- a/router/core/executor.go +++ b/router/core/executor.go @@ -87,6 +87,12 @@ func (b *ExecutorConfigurationBuilder) Build(ctx context.Context, opts *Executor if opts.ApolloCompatibilityFlags.SuppressFetchErrors.Enabled { options.ResolvableOptions.ApolloCompatibilitySuppressFetchErrors = true } + if opts.ApolloCompatibilityFlags.ReplaceUndefinedOpFieldErrors.Enabled { + options.ResolvableOptions.ApolloCompatibilityReplaceUndefinedOpFieldError = true + } + if opts.ApolloCompatibilityFlags.ReplaceInvalidVarErrors.Enabled { + options.ResolvableOptions.ApolloCompatibilityReplaceInvalidVarError = true + } switch opts.RouterEngineConfig.SubgraphErrorPropagation.Mode { case config.SubgraphErrorPropagationModePassthrough: diff --git a/router/core/graph_server.go b/router/core/graph_server.go index 057e8ef145..6076492fc2 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -696,6 +696,7 @@ func (s *graphServer) buildGraphMux(ctx context.Context, OperationHashCache: gm.operationHashCache, ParseKitPoolSize: s.engineExecutionConfiguration.ParseKitPoolSize, IntrospectionEnabled: s.Config.introspection, + ApolloCompatibilityFlags: s.apolloCompatibilityFlags, }) operationPlanner := NewOperationPlanner(executor, gm.planCache) @@ -766,6 +767,7 @@ func (s *graphServer) buildGraphMux(ctx context.Context, TrackSchemaUsageInfo: s.graphqlMetricsConfig.Enabled, ClientHeader: s.clientHeader, ComputeOperationSha256: computeSha256, + ApolloCompatibilityFlags: &s.apolloCompatibilityFlags, }) if s.webSocketConfiguration != nil && s.webSocketConfiguration.Enabled { diff --git a/router/core/graphql_prehandler.go b/router/core/graphql_prehandler.go index 3f050a399e..1bb68e656c 100644 --- a/router/core/graphql_prehandler.go +++ b/router/core/graphql_prehandler.go @@ -57,6 +57,7 @@ type PreHandlerOptions struct { TrackSchemaUsageInfo bool ClientHeader config.ClientHeader ComputeOperationSha256 bool + ApolloCompatibilityFlags *config.ApolloCompatibilityFlags } type PreHandler struct { @@ -88,6 +89,7 @@ type PreHandler struct { trackSchemaUsageInfo bool clientHeader config.ClientHeader computeOperationSha256 bool + apolloCompatibilityFlags *config.ApolloCompatibilityFlags } type httpOperation struct { @@ -133,6 +135,7 @@ func NewPreHandler(opts *PreHandlerOptions) *PreHandler { trackSchemaUsageInfo: opts.TrackSchemaUsageInfo, clientHeader: opts.ClientHeader, computeOperationSha256: opts.ComputeOperationSha256, + apolloCompatibilityFlags: opts.ApolloCompatibilityFlags, } } diff --git a/router/core/http_graphql_error.go b/router/core/http_graphql_error.go index bc72e30536..ee44eef1bd 100644 --- a/router/core/http_graphql_error.go +++ b/router/core/http_graphql_error.go @@ -2,6 +2,8 @@ package core type HttpError interface { error + // ExtensionCode is the code that should be included in the error extensions + ExtensionCode() string // Message represents a human-readable error message to be sent to the client/user Message() string // StatusCode is the status code to be sent to the client @@ -12,14 +14,19 @@ var _ HttpError = (*httpGraphqlError)(nil) // httpGraphqlError is an error that can be used to return a custom GraphQL error message and http status code type httpGraphqlError struct { - message string - statusCode int + extensionCode string + message string + statusCode int } func (e *httpGraphqlError) Error() string { return e.message } +func (e *httpGraphqlError) ExtensionCode() string { + return e.extensionCode +} + func (e *httpGraphqlError) Message() string { return e.message } diff --git a/router/core/operation_processor.go b/router/core/operation_processor.go index b98abac8e2..e687af6197 100644 --- a/router/core/operation_processor.go +++ b/router/core/operation_processor.go @@ -5,6 +5,8 @@ import ( "context" "crypto/sha256" "fmt" + "github.com/wundergraph/cosmo/router/pkg/config" + "github.com/wundergraph/graphql-go-tools/v2/pkg/apollocompatibility" "hash" "io" "net/http" @@ -76,6 +78,10 @@ func (e invalidExtensionsTypeError) StatusCode() int { return http.StatusBadRequest } +func (e invalidExtensionsTypeError) ExtensionCode() string { + return "" +} + var ( _ HttpError = invalidExtensionsTypeError(0) ) @@ -92,6 +98,7 @@ type OperationProcessorOptions struct { OperationHashCache *ristretto.Cache[uint64, string] ParseKitPoolSize int IntrospectionEnabled bool + ApolloCompatibilityFlags config.ApolloCompatibilityFlags } // OperationProcessor provides shared resources to the parseKit and OperationKit. @@ -836,6 +843,14 @@ func (o *OperationKit) Validate(skipLoader bool) (cacheHit bool, err error) { // this is useful to return a query plan without having to provide variables err = o.kit.variablesValidator.Validate(o.kit.doc, o.operationProcessor.executor.ClientSchema, o.kit.doc.Input.Variables) if err != nil { + var invalidVarErr *variablesvalidation.InvalidVariableError + if errors.As(err, &invalidVarErr) { + return false, &httpGraphqlError{ + extensionCode: invalidVarErr.ExtensionCode, + message: invalidVarErr.Error(), + statusCode: http.StatusOK, + } + } return false, &httpGraphqlError{ message: err.Error(), statusCode: http.StatusOK, @@ -928,7 +943,11 @@ func (o *OperationKit) skipIncludeVariableNames() []string { return names } -func createParseKit(i int) *parseKit { +type parseKitOptions struct { + apolloCompatibilityFlags config.ApolloCompatibilityFlags +} + +func createParseKit(i int, options *parseKitOptions) *parseKit { return &parseKit{ i: i, parser: astparser.NewParser(), @@ -944,8 +963,16 @@ func createParseKit(i int) *parseKit { variablesNormalizer: astnormalization.NewVariablesNormalizer(), printer: &astprinter.Printer{}, normalizedOperation: &bytes.Buffer{}, - variablesValidator: variablesvalidation.NewVariablesValidator(), - operationValidator: astvalidation.DefaultOperationValidator(), + variablesValidator: variablesvalidation.NewVariablesValidator(variablesvalidation.VariablesValidatorOptions{ + ApolloCompatibilityFlags: apollocompatibility.Flags{ + ReplaceInvalidVarError: options.apolloCompatibilityFlags.ReplaceInvalidVarErrors.Enabled, + }, + }), + operationValidator: astvalidation.DefaultOperationValidator(astvalidation.WithApolloCompatibilityFlags( + apollocompatibility.Flags{ + ReplaceUndefinedOpFieldError: options.apolloCompatibilityFlags.ReplaceUndefinedOpFieldErrors.Enabled, + }, + )), } } @@ -963,7 +990,7 @@ func NewOperationProcessor(opts OperationProcessorOptions) *OperationProcessor { } for i := 0; i < opts.ParseKitPoolSize; i++ { processor.parseKitSemaphore <- i - processor.parseKits[i] = createParseKit(i) + processor.parseKits[i] = createParseKit(i, &parseKitOptions{apolloCompatibilityFlags: opts.ApolloCompatibilityFlags}) } if opts.EnablePersistedOperationsCache { processor.operationCache = &OperationCache{ diff --git a/router/core/router.go b/router/core/router.go index 03df6a6bd2..dce27299c3 100644 --- a/router/core/router.go +++ b/router/core/router.go @@ -1593,6 +1593,8 @@ func WithApolloCompatibilityFlagsConfig(cfg config.ApolloCompatibilityFlags) Opt cfg.ValueCompletion.Enabled = true cfg.TruncateFloats.Enabled = true cfg.SuppressFetchErrors.Enabled = true + cfg.ReplaceUndefinedOpFieldErrors.Enabled = true + cfg.ReplaceInvalidVarErrors.Enabled = true } r.apolloCompatibilityFlags = cfg } diff --git a/router/go.mod b/router/go.mod index 893d08eda1..ae14a11dd6 100644 --- a/router/go.mod +++ b/router/go.mod @@ -38,7 +38,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.113 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115 // 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 ddfe0decfe..07817c72cd 100644 --- a/router/go.sum +++ b/router/go.sum @@ -268,8 +268,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-20240910140849-bb15f94bd362 h1:MxNSJqQFJyhKwU4xPj6diIRLm+oY1wNbAZW0jJpikBE= github.com/wundergraph/astjson v0.0.0-20240910140849-bb15f94bd362/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113 h1:lo6ZgsLNJF4Qc+TCH4h0KPc48ew5GS6i1rB/iCjRfBg= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.113/go.mod h1:2FThDhi9IFpRv/l/L4BQJM2+Az8TYdq7tFycTPGOuFY= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115 h1:D45Z2XpuQjHtKEDSNX41r1aLkQGS2oB8I43vcDTSmEw= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.115/go.mod h1:2FThDhi9IFpRv/l/L4BQJM2+Az8TYdq7tFycTPGOuFY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index 00dd9dbb0b..fd83b615eb 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -639,10 +639,12 @@ type AccessLogsFileOutputConfig struct { } type ApolloCompatibilityFlags struct { - EnableAll bool `yaml:"enable_all" envDefault:"false" env:"APOLLO_COMPATIBILITY_ENABLE_ALL"` - ValueCompletion ApolloCompatibilityValueCompletion `yaml:"value_completion"` - TruncateFloats ApolloCompatibilityTruncateFloats `yaml:"truncate_floats"` - SuppressFetchErrors ApolloCompatibilitySuppressFetchErrors `yaml:"suppress_fetch_errors"` + EnableAll bool `yaml:"enable_all" envDefault:"false" env:"APOLLO_COMPATIBILITY_ENABLE_ALL"` + ValueCompletion ApolloCompatibilityValueCompletion `yaml:"value_completion"` + TruncateFloats ApolloCompatibilityTruncateFloats `yaml:"truncate_floats"` + SuppressFetchErrors ApolloCompatibilitySuppressFetchErrors `yaml:"suppress_fetch_errors"` + ReplaceUndefinedOpFieldErrors ApolloCompatibilityReplaceUndefinedOpFieldErrors `yaml:"replace_undefined_op_field_errors"` + ReplaceInvalidVarErrors ApolloCompatibilityReplaceInvalidVarErrors `yaml:"replace_invalid_var_errors"` } type ApolloCompatibilityValueCompletion struct { @@ -662,6 +664,14 @@ type ApolloCompatibilitySuppressFetchErrors struct { Enabled bool `yaml:"enabled" envDefault:"false" env:"APOLLO_COMPATIBILITY_SUPPRESS_FETCH_ERRORS_ENABLED"` } +type ApolloCompatibilityReplaceUndefinedOpFieldErrors struct { + Enabled bool `yaml:"enabled" envDefault:"false" env:"APOLLO_COMPATIBILITY_REPLACE_UNDEFINED_OP_FIELD_ERRORS_ENABLED"` +} + +type ApolloCompatibilityReplaceInvalidVarErrors struct { + Enabled bool `yaml:"enabled" envDefault:"false" env:"APOLLO_COMPATIBILITY_REPLACE_INVALID_VAR_ERRORS_ENABLED"` +} + type Config struct { Version string `yaml:"version,omitempty" ignored:"true"` diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index 0a7328ef96..bc795dc531 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -328,6 +328,12 @@ }, "SuppressFetchErrors": { "Enabled": false + }, + "ReplaceUndefinedOpFieldErrors": { + "Enabled": false + }, + "ReplaceInvalidVarErrors": { + "Enabled": false } }, "ClientHeader": { diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index 0fa3b0bded..20bbfe4f2c 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -498,6 +498,12 @@ }, "SuppressFetchErrors": { "Enabled": false + }, + "ReplaceUndefinedOpFieldErrors": { + "Enabled": false + }, + "ReplaceInvalidVarErrors": { + "Enabled": false } }, "ClientHeader": { From 51bcbc5e6836d003afa2ebf3276aa662fb369e2a Mon Sep 17 00:00:00 2001 From: hardworker-bot Date: Tue, 29 Oct 2024 15:09:20 +0000 Subject: [PATCH 2/4] chore(release): Publish [skip ci] - aws-lambda-router@0.36.0 - router@0.134.0 --- aws-lambda-router/CHANGELOG.md | 6 ++++++ aws-lambda-router/package.json | 2 +- router/CHANGELOG.md | 6 ++++++ router/package.json | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/aws-lambda-router/CHANGELOG.md b/aws-lambda-router/CHANGELOG.md index 8b3cdc8251..ef6ce435b8 100644 --- a/aws-lambda-router/CHANGELOG.md +++ b/aws-lambda-router/CHANGELOG.md @@ -4,6 +4,12 @@ Binaries are attached to the github release otherwise all images can be found [h All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.36.0](https://github.com/wundergraph/cosmo/compare/aws-lambda-router@0.35.0...aws-lambda-router@0.36.0) (2024-10-29) + +### Features + +* extend apollo compatible error support ([#1311](https://github.com/wundergraph/cosmo/issues/1311)) ([d4d727e](https://github.com/wundergraph/cosmo/commit/d4d727e1c98f92eaa2103ca2356537e3a63eeff2)) (@Aenimus) + # [0.35.0](https://github.com/wundergraph/cosmo/compare/aws-lambda-router@0.34.2...aws-lambda-router@0.35.0) (2024-10-28) ### Features diff --git a/aws-lambda-router/package.json b/aws-lambda-router/package.json index a95e6474be..c9e772e70d 100644 --- a/aws-lambda-router/package.json +++ b/aws-lambda-router/package.json @@ -1,6 +1,6 @@ { "name": "aws-lambda-router", - "version": "0.35.0", + "version": "0.36.0", "private": true, "description": "Placeholder package to simplify versioning and releasing with lerna.", "keywords": [ diff --git a/router/CHANGELOG.md b/router/CHANGELOG.md index 021c1038f7..fb20d72d1d 100644 --- a/router/CHANGELOG.md +++ b/router/CHANGELOG.md @@ -4,6 +4,12 @@ Binaries are attached to the github release otherwise all images can be found [h All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [0.134.0](https://github.com/wundergraph/cosmo/compare/router@0.133.1...router@0.134.0) (2024-10-29) + +### Features + +* extend apollo compatible error support ([#1311](https://github.com/wundergraph/cosmo/issues/1311)) ([d4d727e](https://github.com/wundergraph/cosmo/commit/d4d727e1c98f92eaa2103ca2356537e3a63eeff2)) (@Aenimus) + ## [0.133.1](https://github.com/wundergraph/cosmo/compare/router@0.133.0...router@0.133.1) (2024-10-29) ### Bug Fixes diff --git a/router/package.json b/router/package.json index 5a722ac300..69717e13fd 100644 --- a/router/package.json +++ b/router/package.json @@ -1,6 +1,6 @@ { "name": "router", - "version": "0.133.1", + "version": "0.134.0", "private": true, "description": "Placeholder package to simplify versioning and releasing with lerna.", "keywords": [ From 290cf9f6bb08e71ec3d1734f40ce2aa7a745a7bd Mon Sep 17 00:00:00 2001 From: Aenimus <47415099+Aenimus@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:41:10 +0000 Subject: [PATCH 3/4] fix: propagate new yaml properties in schema.json (#1322) --- router/pkg/config/config.schema.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index dd26e37354..aba7f05c2a 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -1937,6 +1937,28 @@ "default": false } } + }, + "replace_undefined_op_field_errors": { + "type": "object", + "description": "Produces the same error (message, extension code, status code) as Apollo when an invalid operation field is included in an operation selection set.", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "default": false + } + } + }, + "replace_invalid_var_errors": { + "type": "object", + "description": "Produces the same error (message, extension code, status code) as Apollo when an invalid variable is supplied.", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "default": false + } + } } } }, From 82b9c284cee936724874a0efbac2ed7207ae54b5 Mon Sep 17 00:00:00 2001 From: hardworker-bot Date: Tue, 29 Oct 2024 16:45:26 +0000 Subject: [PATCH 4/4] chore(release): Publish [skip ci] - router@0.134.1 --- router/CHANGELOG.md | 6 ++++++ router/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/router/CHANGELOG.md b/router/CHANGELOG.md index fb20d72d1d..df5ca8c775 100644 --- a/router/CHANGELOG.md +++ b/router/CHANGELOG.md @@ -4,6 +4,12 @@ Binaries are attached to the github release otherwise all images can be found [h All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [0.134.1](https://github.com/wundergraph/cosmo/compare/router@0.134.0...router@0.134.1) (2024-10-29) + +### Bug Fixes + +* propagate new yaml properties in schema.json ([#1322](https://github.com/wundergraph/cosmo/issues/1322)) ([290cf9f](https://github.com/wundergraph/cosmo/commit/290cf9f6bb08e71ec3d1734f40ce2aa7a745a7bd)) (@Aenimus) + # [0.134.0](https://github.com/wundergraph/cosmo/compare/router@0.133.1...router@0.134.0) (2024-10-29) ### Features diff --git a/router/package.json b/router/package.json index 69717e13fd..1833e7eca5 100644 --- a/router/package.json +++ b/router/package.json @@ -1,6 +1,6 @@ { "name": "router", - "version": "0.134.0", + "version": "0.134.1", "private": true, "description": "Placeholder package to simplify versioning and releasing with lerna.", "keywords": [