Repository: apollographql/apollo-server
Pull request #7469 (2023-03-25 23:13:07)

Update directives.md
Update documentation to show that deprecated arguments must be optional as per GraphQL specification

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy request for *apollo-server-docs* pending review.
[Visit the deploys page to approve it](https://app.netlify.com/sites/apollo-server-docs/deploys)

|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 5d7c552d624039e24e5c6970789f32b290a2293a |
apollo-cla: @akinboboye: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/
codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/apollographql/apollo-server/pr/7469/builds/361208) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 5d7c552d624039e24e5c6970789f32b290a2293a:

|Sandbox| Source |
|--|--|
|[Apollo Server Typescript](https://codesandbox.io/s/apollo-server-typescript-bhn3m3)| Configuration |
|[Apollo Server](https://codesandbox.io/s/apollo-server-6ccy61)| Configuration |


Patch:
diff --git a/.changeset/purple-paws-yell.md b/.changeset/purple-paws-yell.md
new file mode 100644
index 000000000..86ed8997d
--- /dev/null
+++ b/.changeset/purple-paws-yell.md
@@ -0,0 +1,18 @@
+---
+'@apollo/server': minor
+---
+
+Introduce new opt-in configuration option to mitigate v4 status code regression
+
+Apollo Server v4 accidentally started responding to requests with an invalid `variables` object with a 200 status code, where v3 previously responded with a 400. In order to not break current behavior (potentially breaking users who have creatively worked around this issue) and offer a mitigation, we've added the following configuration option which we recommend for all users.
+
+```ts
+new ApolloServer({
+  // ...
+  status400ForVariableCoercionErrors: true,
+});
+```
+
+Specifically, this regression affects cases where _input variable coercion_ fails. Variables of an incorrect type (i.e. `String` instead of `Int`) or unexpectedly `null` are examples that fail variable coercion. Additionally, missing or incorrect fields on input objects as well as custom scalars that throw during validation will also fail variable coercion. For more specifics on variable coercion, see the "Input Coercion" sections in the [GraphQL spec](https://spec.graphql.org/June2018/#sec-Scalars).
+
+This will become the default behavior in Apollo Server v5 and the configuration option will be ignored / no longer needed.
diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx
index d0d65be14..80ee4848e 100644
--- a/docs/source/api/apollo-server.mdx
+++ b/docs/source/api/apollo-server.mdx
@@ -475,6 +475,23 @@ If this is set to any string value, use that value instead of the environment va
 </td>
 </tr>
 
+<tr>
+<td>
+
+###### `status400ForVariableCoercionErrors`
+
+`boolean`
+
+</td>
+<td>
+
+**Set this option to `true`.** It mitigates a regression introduced in Apollo Server v4 where the server returns a 200 status code (instead of 400) when a client query provides invalid variables. [Learn more.](../migration/#known-regressions)
+
+Apollo Server v5 will _always_ behave as if this option is `true` (and will ignore any provided value).
+
+</td>
+</tr>
+
 </tbody>
 </table>
 
diff --git a/docs/source/data/errors.mdx b/docs/source/data/errors.mdx
index 58afeaeb9..f7df300ab 100644
--- a/docs/source/data/errors.mdx
+++ b/docs/source/data/errors.mdx
@@ -5,6 +5,8 @@ description: Making errors actionable on the client and server
 
 import TopLevelAwait from "../shared/top-level-await.mdx"
 
+> Apollo Server v4 introduced a regression where providing invalid variables yields a 200 status code instead of 400. To mitigate this regression, provide the `status400ForVariableCoercionErrors: true` option to your `ApolloServer` constructor. For more information, see the [migration guide](../migration/#known-regressions).
+
 <!-- cSpell:ignore typenam -->
 
 Whenever Apollo Server encounters errors while processing a GraphQL operation, its response to the client includes an `errors` array containing each error that occurred. Each error in the array has an `extensions` field that provides additional useful information, including an error `code` and (while in development mode) a `stacktrace`.
diff --git a/docs/source/migration.mdx b/docs/source/migration.mdx
index fc00337b8..de6b893d4 100644
--- a/docs/source/migration.mdx
+++ b/docs/source/migration.mdx
@@ -344,6 +344,29 @@ You can also import each plugin's associated TypeScript types (e.g., `ApolloServ
 
 Once you've updated your imports, you can remove your project's dependency on `apollo-server-core`.
 
+## Known regressions
+
+### Appropriate 400 status codes
+
+Apollo Server v4 responds to an invalid `variables` object with a 200 status code, whereas v3 responds appropriately with a 400 status code. This regression was introduced in [PR #6502](https://github.com/apollographql/apollo-server/pull/6502) and brought to our attention in [Issue #7462](https://github.com/apollographql/apollo-server/issues/7462).
+
+Specifically, this regression affects cases where _input variable coercion_ fails. Variables of an incorrect type (i.e. `String` instead of `Int`) or unexpectedly `null` are examples that fail variable coercion. Additionally, missing or incorrect fields on input objects as well as custom scalars that throw during validation will also fail variable coercion. For additional specifics on variable coercion, see the "Input Coercion" sections in the [GraphQL spec](https://spec.graphql.org/June2018/#sec-Scalars).
+
+We recommend mitigating this regression unless you've already modified your application to work around it. To do so, add the `status400ForVariableCoercionErrors: true` option to your `ApolloServer` constructor:
+
+<MultiCodeBlock>
+
+```ts
+new ApolloServer({
+  // ...
+  status400ForVariableCoercionErrors: true,
+})
+```
+
+</MultiCodeBlock>
+
+This option will no longer be needed (and will be ignored) in Apollo Server v5.
+
 ## Bumped dependencies
 
 ### Node.js
diff --git a/docs/source/schema/directives.md b/docs/source/schema/directives.md
index aa0d8ecc5..cc3c91583 100644
--- a/docs/source/schema/directives.md
+++ b/docs/source/schema/directives.md
@@ -45,7 +45,7 @@ directive @withDeprecatedArgs(
 
 type MyType {
   # ARGUMENT_DEFINITION (alternate example on a field's args)
-  fieldWithDeprecatedArgs(name: String! @deprecated): String
+  fieldWithDeprecatedArgs(name: String @deprecated): String
   # FIELD_DEFINITION
   deprecatedField: String @deprecated
 }
diff --git a/docs/source/shared/integration-table.mdx b/docs/source/shared/integration-table.mdx
index 5db0e26d5..6ed4c2cc8 100644
--- a/docs/source/shared/integration-table.mdx
+++ b/docs/source/shared/integration-table.mdx
@@ -1,9 +1,10 @@
 | Web Framework | Integration Package |
 |--------|-------------|
 | [AWS Lambda](https://aws.amazon.com/lambda/) | [`@as-integrations/aws-lambda`](https://www.npmjs.com/package/@as-integrations/aws-lambda) |
+| [Azure Functions](https://azure.microsoft.com/en-us/services/functions/) | [`@as-integrations/azure-functions`](https://www.npmjs.com/package/@as-integrations/azure-functions) |
+| [Cloudflare Workers](https://workers.cloudflare.com/) | [`@as-integrations/cloudflare-workers`](https://www.npmjs.com/package/@as-integrations/cloudflare-workers) |
 | [Fastify](https://fastify.io/) | [`@as-integrations/fastify`](https://www.npmjs.com/package/@as-integrations/fastify) |
 | [Hapi](https://hapi.dev/) | [`@as-integrations/hapi`](https://www.npmjs.com/package/@as-integrations/hapi) |
 | [Koa](https://koajs.com/) | [`@as-integrations/koa`](https://www.npmjs.com/package/@as-integrations/koa) |
 | [Next.js](https://nextjs.org) | [`@as-integrations/next`](https://www.npmjs.com/package/@as-integrations/next) |
 | [Nuxt](https://v3.nuxtjs.org/) / [h3](https://github.com/unjs/h3) | [`@as-integrations/h3`](https://www.npmjs.com/package/@as-integrations/h3) |
-| [Cloudflare Workers](https://workers.cloudflare.com/) | [`@as-integrations/cloudflare-workers`](https://www.npmjs.com/package/@as-integrations/cloudflare-workers) |
diff --git a/package-lock.json b/package-lock.json
index d0d5624c5..dc591f8fa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,7 +33,7 @@
         "@types/lodash.sumby": "4.6.7",
         "@types/lodash.xorby": "4.7.7",
         "@types/negotiator": "0.6.1",
-        "@types/node": "14.18.40",
+        "@types/node": "14.18.41",
         "@types/node-fetch": "2.6.2",
         "@types/qs-middleware": "1.0.1",
         "@types/retry": "0.12.2",
@@ -4746,9 +4746,9 @@
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "14.18.40",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.40.tgz",
-      "integrity": "sha512-pGteXO/JQX7wPxGR8lyT+doqjMa7XvlVowwrDwLfX92k5SdLkk4cwC7CYSLBxrenw/R5oQwKioVIak7ZgplM3g=="
+      "version": "14.18.41",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.41.tgz",
+      "integrity": "sha512-2cfHr8AsUjKx6u4Q+d2eqK51z8+HueoumCQGCKVt95y/yGG4uajOuCANSnE20mbLw94h3tMcddIJ8nYkTu2mFw=="
     },
     "node_modules/@types/node-fetch": {
       "version": "2.6.2",
@@ -18430,9 +18430,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "14.18.40",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.40.tgz",
-      "integrity": "sha512-pGteXO/JQX7wPxGR8lyT+doqjMa7XvlVowwrDwLfX92k5SdLkk4cwC7CYSLBxrenw/R5oQwKioVIak7ZgplM3g=="
+      "version": "14.18.41",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.41.tgz",
+      "integrity": "sha512-2cfHr8AsUjKx6u4Q+d2eqK51z8+HueoumCQGCKVt95y/yGG4uajOuCANSnE20mbLw94h3tMcddIJ8nYkTu2mFw=="
     },
     "@types/node-fetch": {
       "version": "2.6.2",
diff --git a/package.json b/package.json
index d6f3f7019..726de7979 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
     "@types/lodash.sumby": "4.6.7",
     "@types/lodash.xorby": "4.7.7",
     "@types/negotiator": "0.6.1",
-    "@types/node": "14.18.40",
+    "@types/node": "14.18.41",
     "@types/node-fetch": "2.6.2",
     "@types/qs-middleware": "1.0.1",
     "@types/retry": "0.12.2",
diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts
index 64ab795d6..c77b53b1a 100644
--- a/packages/server/src/ApolloServer.ts
+++ b/packages/server/src/ApolloServer.ts
@@ -174,7 +174,9 @@ export interface ApolloServerInternals<TContext extends BaseContext> {
   rootValue?: ((parsedQuery: DocumentNode) => unknown) | unknown;
   validationRules: Array<ValidationRule>;
   fieldResolver?: GraphQLFieldResolver<any, TContext>;
-
+  // TODO(AS5): remove OR warn + ignore with this option set, ignore option and
+  // flip default behavior.
+  status400ForVariableCoercionErrors?: boolean;
   __testing_incrementalExecutionResults?: GraphQLExperimentalIncrementalExecutionResults;
 }
 
@@ -325,6 +327,8 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
           ? null
           : config.csrfPrevention.requestHeaders ??
             recommendedCsrfPreventionRequestHeaders,
+      status400ForVariableCoercionErrors:
+        config.status400ForVariableCoercionErrors ?? false,
       __testing_incrementalExecutionResults:
         config.__testing_incrementalExecutionResults,
     };
diff --git a/packages/server/src/externalTypes/constructor.ts b/packages/server/src/externalTypes/constructor.ts
index 3f012f214..7ab4addf8 100644
--- a/packages/server/src/externalTypes/constructor.ts
+++ b/packages/server/src/externalTypes/constructor.ts
@@ -102,6 +102,15 @@ interface ApolloServerOptionsBase<TContext extends BaseContext> {
   // parsing the schema.
   parseOptions?: ParseOptions;
 
+  // TODO(AS5): remove OR warn + ignore with this option set, ignore option and
+  // flip default behavior. Default false. This opt-in configuration fixes a
+  // regression introduced in v4. In v3, Apollo Server would correctly respond
+  // to a request with invalid `variables` with a 400 status code. AS4 responds
+  // with a 200 status code by default. We recommend setting this to `true`
+  // unless you've explicitly worked around this regression already (and maybe
+  // consider undoing the workaround).
+  status400ForVariableCoercionErrors?: boolean;
+
   // For testing only.
   __testing_incrementalExecutionResults?: GraphQLExperimentalIncrementalExecutionResults;
 }
diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts
index 663e08a37..fbd9014e6 100644
--- a/packages/server/src/requestPipeline.ts
+++ b/packages/server/src/requestPipeline.ts
@@ -474,6 +474,19 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
       const { formattedErrors, httpFromErrors } = resultErrors
         ? formatErrors(resultErrors)
         : { formattedErrors: undefined, httpFromErrors: newHTTPGraphQLHead() };
+
+      // TODO(AS5) This becomes the default behavior and the
+      // `status400ForVariableCoercionErrors` configuration option is removed /
+      // ignored.
+      if (
+        internals.status400ForVariableCoercionErrors &&
+        resultErrors?.length &&
+        result.data === undefined &&
+        !httpFromErrors.status
+      ) {
+        httpFromErrors.status = 400;
+      }
+
       mergeHTTPGraphQLHead(requestContext.response.http, httpFromErrors);
 
       if ('singleResult' in fullResult) {


Test patch:
diff --git a/packages/server/src/__tests__/ApolloServer.test.ts b/packages/server/src/__tests__/ApolloServer.test.ts
index dd113979e..0656e9026 100644
--- a/packages/server/src/__tests__/ApolloServer.test.ts
+++ b/packages/server/src/__tests__/ApolloServer.test.ts
@@ -23,6 +23,17 @@ const typeDefs = gql`
     hello: String
     error: Boolean
     contextFoo: String
+    needsStringArg(aString: String): String
+    needsCompoundArg(aCompound: CompoundInput): String
+  }
+
+  input CompoundInput {
+    compound: NestedInput!
+  }
+
+  input NestedInput {
+    nested1: String!
+    nested2: String!
   }
 `;
 
@@ -378,6 +389,61 @@ describe('ApolloServer executeOperation', () => {
     await server.stop();
   });
 
+  // TODO(AS5): expect an update here when default flips
+  it.each([
+    { status400ForVariableCoercionErrors: false, expectedStatus: undefined },
+    { status400ForVariableCoercionErrors: true, expectedStatus: 400 },
+  ])(
+    'variable coercion errors',
+    async ({ status400ForVariableCoercionErrors, expectedStatus }) => {
+      const server = new ApolloServer({
+        typeDefs,
+        resolvers,
+        status400ForVariableCoercionErrors,
+      });
+      await server.start();
+
+      const { body, http } = await server.executeOperation({
+        query: 'query NeedsArg($arg: String) { needsStringArg(aString: $arg) }',
+        variables: { arg: 1 },
+      });
+      const result = singleResult(body);
+      expect(result.errors?.[0].extensions?.code).toBe('BAD_USER_INPUT');
+      expect(http.status).toBe(expectedStatus);
+      await server.stop();
+    },
+  );
+
+  // CompoundInput is { compound: { nested1: String!, nested2: String! }! }
+  // absence, null, and non-string values will all cause coercion errors
+  it.each([
+    { arg: { compound: { nested1: 'abc' } } },
+    { arg: { compound: { nested1: 'abc', nested2: null } } },
+    { arg: { compound: { nested1: 'abc', nested2: 123 } } },
+    {},
+    null,
+    undefined,
+  ])('variable coercion errors, additional examples: %s', async (variables) => {
+    const server = new ApolloServer({
+      typeDefs,
+      resolvers,
+      status400ForVariableCoercionErrors: true,
+    });
+    await server.start();
+
+    const { body, http } = await server.executeOperation({
+      query: `#graphql
+        query NeedsArg($arg: CompoundInput!) { needsCompoundArg(aCompound: $arg) }
+      `,
+      // @ts-expect-error for `null` case
+      variables,
+    });
+    const result = singleResult(body);
+    expect(result.errors?.[0].extensions?.code).toBe('BAD_USER_INPUT');
+    expect(http.status).toBe(400);
+    await server.stop();
+  });
+
   it('passes its second argument as context object', async () => {
     const server = new ApolloServer({
       typeDefs,