Repository: apollographql/apollo-server (ts)

Issue:
Error from GraphQLScalarType is not properly propagated to formatError hook
Hello,
we have a problem when throwing a `GraphQLError` from `GraphQLScalarType` in the new apollo v4. Before (in v3) it properly propagated the error into `formatError` hook but now it always changes its `extensions.code` to `BAD_USER_INPUT` and removes all additional fields.

This is happening only on errors thrown from `GraphQLScalarType` (e.g. errors from mutation/query resolvers work just fine) and only when we define GQL schema using `GraphQLSchema`. I tried [this example](https://www.apollographql.com/docs/apollo-server/schema/custom-scalars#example-restricting-integers-to-odd-values) where the schema is defined using  `typeDefs` and `resolvers` keys and it also worked.

<details>
<summary>Here is the code where you can reproduce the error.</summary>

```js
const { unwrapResolverError } = require('@apollo/server/errors');
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const { GraphQLScalarType, GraphQLError } = require('graphql');
const { GraphQLSchema, GraphQLObjectType, GraphQLInputObjectType } = require('graphql');

const ColorScalarField = new GraphQLScalarType({
  name: 'ScalarField',
  serialize: (value) => value,
  parseValue: () => {
    console.log("Serializing")

    throw new GraphQLError('Scalar field custom error', {
      extensions: { code: 'CUSTOM_ERROR_CODE', extraErrorField: 123 },
    });
  },
  parseValue: () => {
    console.log("Parsing value")

    throw new GraphQLError('Scalar field custom error', {
      extensions: { code: 'CUSTOM_ERROR_CODE', extraErrorField: 123 },
    });
  },
  parseLiteral: () => {
    console.log("Parsing literal")

    throw new GraphQLError('Scalar field custom error', {
      extensions: { code: 'CUSTOM_ERROR_CODE', extraErrorField: 123 },
    });
  },
});

const ItemType = new GraphQLObjectType({
  name: 'item',
  fields: () => ({
    color: {
      type: ColorScalarField,
    },
  }),
});

const ItemUpdateType = new GraphQLInputObjectType({
  name: 'ItemUpdate',
  fields: () => ({
    color: {
      type: ColorScalarField,
    },
  }),
});

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    fields: () => ({
      item: {
        type: ItemType,
        description: 'Fetch the logged in user.',
        resolve: () => {},
      }
    }),
  }),
  mutation: new GraphQLObjectType({
    name: 'Mutation',
    fields: () => ({
      itemUpdate: {
        name: 'itemUpdate',
        args: {
          input: {
            type: ItemUpdateType,
          },
        },
        type: ItemType,
        resolve: () => {},
      }
    }),
  }),
});

const server = new ApolloServer({
  schema,
  debug: true,
  formatError: (err, err2) => {
    console.log("FORMAT ERROR:", err, err2)
    console.log("UNWRAPPED ERROR:", unwrapResolverError(err), unwrapResolverError(err2))

    return { err: 123 };
  }
});

(async () => {
  const { url } = await startStandaloneServer(server);
  console.log(`🚀 Server listening at: ${url}`);

  const response = await server.executeOperation({
    query: `
      mutation itemUpdate($input: ItemUpdate!) {
        itemUpdate(input: $input) {
          color
        }
      }
    `,
    variables: {
      input: {
        color: 'red',
      },
    },
  });

  console.log(response.body.singleResult.data)
  console.log(response.body.singleResult.errors)
})();
```
</details>

<details>

<summary>And here is the output.</summary>

```
Parsing value
FORMAT ERROR: {
  message: 'Variable "$input" got invalid value "red" at "input.color"; Scalar field custom error',
  locations: [ { line: 2, column: 27 } ],
  extensions: {
    code: 'BAD_USER_INPUT',
    stacktrace: [
      'GraphQLError: Variable "$input" got invalid value "red" at "input.color"; Scalar field custom error',
      '    at /test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:147:11',
      '    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:154:9)',
      '    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:117:34)',
      '    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:49:14)',
      '    at coerceInputValue (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:32:10)',
      '    at coerceVariableValues (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:132:69)',
      '    at getVariableValues (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:45:21)',
      '    at buildExecutionContext (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/execute.js:280:63)',
      '    at execute (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/execute.js:116:22)',
      '    at executeIncrementally (/test/node_modules/.pnpm/@apollo+server@4.1.1_graphql@16.6.0/node_modules/@apollo/server/dist/cjs/incrementalDeliveryPolyfill.js:47:34)'
    ]
  }
} GraphQLError: Variable "$input" got invalid value "red" at "input.color"; Scalar field custom error
    at /test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:147:11
    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:154:9)
    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:117:34)
    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:49:14)
    at coerceInputValue (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:32:10)
    at coerceVariableValues (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:132:69)
    at getVariableValues (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:45:21)
    at buildExecutionContext (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/execute.js:280:63)
    at execute (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/execute.js:116:22)
    at executeIncrementally (/test/node_modules/.pnpm/@apollo+server@4.1.1_graphql@16.6.0/node_modules/@apollo/server/dist/cjs/incrementalDeliveryPolyfill.js:47:34) {
  path: undefined,
  locations: [ { line: 2, column: 27 } ],
  extensions: { code: 'BAD_USER_INPUT' }
}
UNWRAPPED ERROR: {
  message: 'Variable "$input" got invalid value "red" at "input.color"; Scalar field custom error',
  locations: [ { line: 2, column: 27 } ],
  extensions: {
    code: 'BAD_USER_INPUT',
    stacktrace: [
      'GraphQLError: Variable "$input" got invalid value "red" at "input.color"; Scalar field custom error',
      '    at /test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:147:11',
      '    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:154:9)',
      '    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:117:34)',
      '    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:49:14)',
      '    at coerceInputValue (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:32:10)',
      '    at coerceVariableValues (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:132:69)',
      '    at getVariableValues (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:45:21)',
      '    at buildExecutionContext (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/execute.js:280:63)',
      '    at execute (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/execute.js:116:22)',
      '    at executeIncrementally (/test/node_modules/.pnpm/@apollo+server@4.1.1_graphql@16.6.0/node_modules/@apollo/server/dist/cjs/incrementalDeliveryPolyfill.js:47:34)'
    ]
  }
} GraphQLError: Variable "$input" got invalid value "red" at "input.color"; Scalar field custom error
    at /test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:147:11
    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:154:9)
    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:117:34)
    at coerceInputValueImpl (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:49:14)
    at coerceInputValue (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/utilities/coerceInputValue.js:32:10)
    at coerceVariableValues (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:132:69)
    at getVariableValues (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/values.js:45:21)
    at buildExecutionContext (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/execute.js:280:63)
    at execute (/test/node_modules/.pnpm/graphql@16.6.0/node_modules/graphql/execution/execute.js:116:22)
    at executeIncrementally (/test/node_modules/.pnpm/@apollo+server@4.1.1_graphql@16.6.0/node_modules/@apollo/server/dist/cjs/incrementalDeliveryPolyfill.js:47:34) {
  path: undefined,
  locations: [ { line: 2, column: 27 } ],
  extensions: { code: 'BAD_USER_INPUT' }
}
undefined
[ { err: 123 } ]
```
</details>

I would expect the error received in `formatError` hook to have `extensions.code === 'CUSTOM_ERROR_CODE'` and `extensions.extraErrorField === 123`.

Is there anything we missed? Thank you for any help.

Pull request:
Don't override custom code extensions from scalar parseValue errors
Adding `BAD_USER_INPUT` is a nice default (and overrides the inappropriate default of `INTERNAL_SERVER_ERROR`) but if somebody has set a `code` already, we shouldn't override.

(Note that there's a separate issue where graphql-js throws out extensions from the thrown error itself and only pays attention to extensions on the error's originalError; we're trying to fix that in https://github.com/graphql/graphql-js/pull/3785 but this is orthogonal.)

Fixes #7178.


Patch:
diff --git a/.changeset/tiny-deers-compare.md b/.changeset/tiny-deers-compare.md
new file mode 100644
index 000000000..9e8da1654
--- /dev/null
+++ b/.changeset/tiny-deers-compare.md
@@ -0,0 +1,6 @@
+---
+'@apollo/server-integration-testsuite': patch
+'@apollo/server': patch
+---
+
+Apollo Server tries to detect if execution errors are variable coercion errors in order to give them a `code` extension of `BAD_USER_INPUT` rather than `INTERNAL_SERVER_ERROR`. Previously this would unconditionally set the `code`; now, it only sets the `code` if no `code` is already set, so that (for example) custom scalar `parseValue` methods can throw errors with specific `code`s. (Note that a separate graphql-js bug can lead to these extensions being lost; see https://github.com/graphql/graphql-js/pull/3785 for details.)
diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts
index 48e764431..a40406c20 100644
--- a/packages/server/src/requestPipeline.ts
+++ b/packages/server/src/requestPipeline.ts
@@ -447,13 +447,18 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
       // variables are required and get non-null values. If any of these things
       // 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.
+      // errors. (But if the error already has a code, perhaps because the
+      // original error was thrown from a custom scalar parseValue, we leave it
+      // alone. We check that here instead of as part of
+      // isBadUserInputGraphQLError since perhaps that function will one day be
+      // changed to something we can get directly from graphql-js, but the
+      // `code` check is AS-specific.)
       //
       // 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 (isBadUserInputGraphQLError(e)) {
+        if (isBadUserInputGraphQLError(e) && e.extensions?.code == null) {
           return new UserInputError(e);
         }
         return e;


Test patch:
diff --git a/packages/integration-testsuite/src/apolloServerTests.ts b/packages/integration-testsuite/src/apolloServerTests.ts
index b36ba8101..41d5e0613 100644
--- a/packages/integration-testsuite/src/apolloServerTests.ts
+++ b/packages/integration-testsuite/src/apolloServerTests.ts
@@ -17,6 +17,7 @@ import {
   printSchema,
   FieldNode,
   GraphQLFormattedError,
+  GraphQLScalarType,
 } from 'graphql';
 
 // Note that by doing deep imports here we don't need to install React.
@@ -467,6 +468,106 @@ export function defineIntegrationTestSuiteApolloServerTests(
           );
           expect(result.errors[0].extensions.code).toBe('BAD_USER_INPUT');
         });
+
+        it('catches custom scalar parseValue and returns BAD_USER_INPUT', async () => {
+          const uri = await createServerAndGetUrl({
+            typeDefs: gql`
+              scalar CustomScalar
+              type Query {
+                hello(x: CustomScalar): String
+              }
+            `,
+            resolvers: {
+              CustomScalar: new GraphQLScalarType({
+                name: 'CustomScalar',
+                parseValue() {
+                  // Work-around for https://github.com/graphql/graphql-js/pull/3785
+                  // Once that's fixed, we can just directly throw this error.
+                  const e = new GraphQLError('Something bad happened', {
+                    extensions: { custom: 'foo' },
+                  });
+                  throw new GraphQLError(e.message, { originalError: e });
+                },
+              }),
+            },
+          });
+
+          const apolloFetch = createApolloFetch({ uri });
+
+          const result = await apolloFetch({
+            query: `query ($x:CustomScalar) {hello(x:$x)}`,
+            variables: { x: 'foo' },
+          });
+          expect(result).toMatchInlineSnapshot(`
+            {
+              "errors": [
+                {
+                  "extensions": {
+                    "code": "BAD_USER_INPUT",
+                    "custom": "foo",
+                  },
+                  "locations": [
+                    {
+                      "column": 8,
+                      "line": 1,
+                    },
+                  ],
+                  "message": "Variable "$x" got invalid value "foo"; Something bad happened",
+                },
+              ],
+            }
+          `);
+        });
+
+        it('catches custom scalar parseValue and preserves code', async () => {
+          const uri = await createServerAndGetUrl({
+            typeDefs: gql`
+              scalar CustomScalar
+              type Query {
+                hello(x: CustomScalar): String
+              }
+            `,
+            resolvers: {
+              CustomScalar: new GraphQLScalarType({
+                name: 'CustomScalar',
+                parseValue() {
+                  // Work-around for https://github.com/graphql/graphql-js/pull/3785
+                  // Once that's fixed, we can just directly throw this error.
+                  const e = new GraphQLError('Something bad happened', {
+                    extensions: { custom: 'foo', code: 'CUSTOMIZED' },
+                  });
+                  throw new GraphQLError(e.message, { originalError: e });
+                },
+              }),
+            },
+          });
+
+          const apolloFetch = createApolloFetch({ uri });
+
+          const result = await apolloFetch({
+            query: `query ($x:CustomScalar) {hello(x:$x)}`,
+            variables: { x: 'foo' },
+          });
+          expect(result).toMatchInlineSnapshot(`
+            {
+              "errors": [
+                {
+                  "extensions": {
+                    "code": "CUSTOMIZED",
+                    "custom": "foo",
+                  },
+                  "locations": [
+                    {
+                      "column": 8,
+                      "line": 1,
+                    },
+                  ],
+                  "message": "Variable "$x" got invalid value "foo"; Something bad happened",
+                },
+              ],
+            }
+          `);
+        });
       });
 
       describe('schema creation', () => {