diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab0db6bc86..93795cc3396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ The version headers in this history reflect the versions of Apollo Server itself ## vNEXT +- `apollo-server-core`: If a client does not provide a value or provides null for a variable declared to be non-null, this is now reported as an error with an `extensions.code` of `BAD_USER_INPUT` rather than `INTERNAL_SERVER_ERROR`. (This is similar to a change we made in v2.23.0 for variables that are sent as the wrong type.) [PR #5508](https://github.com/apollographql/apollo-server/pull/5508) [Issue #5353](https://github.com/apollographql/apollo-server/issues/5353) + + ## v3.0.2 - `apollo-server-types`: TypeScript typings for `info.cacheControl` are now added to `GraphQLResolveInfo` as part of `apollo-server-types` rather than a nested file in `apollo-server-core`, and the field now has a named type, `ResolveInfoCacheControl`. [PR #5512](https://github.com/apollographql/apollo-server/pull/5512) diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index a84360c4af8..a78172a976b 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -378,16 +378,32 @@ export async function processGraphQLRequest( // The first thing that execution does is coerce the request's variables // to the types declared in the operation, which can lead to errors if - // they are of the wrong type. We change any such errors into - // UserInputError so that their code doesn't end up being - // INTERNAL_SERVER_ERROR, since these are client errors. + // they are of the wrong type. It also makes sure that all non-null + // variables are required and get non-null values. If any of these thingss + // lead to errors, we change them into UserInputError so that their code + // doesn't end up being INTERNAL_SERVER_ERROR, since these are client + // errors. + // + // This is hacky! Hopefully graphql-js will give us a way to separate + // variable resolution from execution later; see + // https://github.com/graphql/graphql-js/issues/3169 const resultErrors = result.errors?.map((e) => { if ( e.nodes?.length === 1 && e.nodes[0].kind === Kind.VARIABLE_DEFINITION && - e.message.startsWith( + (e.message.startsWith( `Variable "$${e.nodes[0].variable.name.value}" got invalid value `, - ) + ) || + (e.nodes[0].type.kind === Kind.NON_NULL_TYPE && + e.nodes[0].type.type.kind === Kind.NAMED_TYPE && + (e.message.startsWith( + `Variable "$${e.nodes[0].variable.name.value}" of required ` + + `type "${e.nodes[0].type.type.name.value}!" was not provided.`, + ) || + e.message.startsWith( + `Variable "$${e.nodes[0].variable.name.value}" of non-null ` + + `type "${e.nodes[0].type.type.name.value}!" must not be null.`, + )))) ) { return fromGraphQLError(e, { errorClass: UserInputError, diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts index 5564f0cbaf4..e3fe23a8c27 100644 --- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts +++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts @@ -343,6 +343,51 @@ export function testApolloServer( expect(result.errors[0].extensions.code).toBe('BAD_USER_INPUT'); }); + it('catches required type variable error and returns UserInputError', async () => { + const { url: uri } = await createApolloServer({ + typeDefs: gql` + type Query { + hello(x: String!): String + } + `, + }); + + const apolloFetch = createApolloFetch({ uri }); + + const result = await apolloFetch({ + query: `query ($x:String!) {hello(x:$x)}`, + }); + expect(result.data).toBeUndefined(); + expect(result.errors).toBeDefined(); + expect(result.errors[0].message).toMatch( + `Variable "$x" of required type "String!" was not provided.`, + ); + expect(result.errors[0].extensions.code).toBe('BAD_USER_INPUT'); + }); + + it('catches non-null type variable error and returns UserInputError', async () => { + const { url: uri } = await createApolloServer({ + typeDefs: gql` + type Query { + hello(x: String!): String + } + `, + }); + + const apolloFetch = createApolloFetch({ uri }); + + const result = await apolloFetch({ + query: `query ($x:String!) {hello(x:$x)}`, + variables: { x: null }, + }); + expect(result.data).toBeUndefined(); + expect(result.errors).toBeDefined(); + expect(result.errors[0].message).toMatch( + `Variable "$x" of non-null type "String!" must not be null.`, + ); + expect(result.errors[0].extensions.code).toBe('BAD_USER_INPUT'); + }); + describe('schema creation', () => { it('accepts typeDefs and resolvers', async () => { const typeDefs = gql`