Repository: apollographql/apollo-server (ts)

Issue:
Warn when reporting usage metrics from a subgraph to GraphOS, or when attempting to report metrics to a cloud-hosted supergraph in GraphOS
### Context

Right now, it's possible to use the `ApolloServerUsageReportingPlugin` to report usage metrics to cloud-hosted supergraphs on GraphOS. This happens automatically if you run your Apollo Server with an `APOLLO_KEY` and `APOLLO_GRAPH_REF` in environment variables. This creates an issue on the GraphOS side because the router in the cloud-hosted supergraph is also reporting metrics, so it creates double counts the requests.

We're going to do some work on the cloud side to disallow reporting self-hosted usage metrics from an Apollo Server to cloud-hosted supergraphs. At the same time, I think it'd be nice of Apollo Server could warn against this to the extent it can detect the issue happening.

### Suggestions

1. If Apollo Server detects that it's running a subgraph (ie, a schema with a _Service.sdl: string field), it would be nice if it printed a warning if the usage reporting plugin is enabled. It could also be nice if usage reporting from the subgraph required being specifically enabled with the plugin as opposed to just the environment variables.
2. If Apollo Server is able to detect that the API key it has is from a cloud-hosted supergraph (maybe it makes a call to the Studio API?), then it'd be really nice if we logged an error along these lines: "Whoops! It looks like you're trying to use an API key associated to a serverless supergraph. See the docs here to learn more about how metrics work in Apollo Studio and how to configure your supergraph or subgraph servers properly https://www.apollographql.com/docs/apollo-server/monitoring/metrics/#connecting-to-apollo-studio"

Pull request description:
Don't install the usage reporting plugin by default in subgraphs
The usage reporting plugin is designed for monolithic graphs and
Gateways, not for subgraphs. You will get strange results in
GraphOS/Studio if you send usage reports from a subgraph to Studio. This
PR changes the defaults so that subgraphs do not automatically install
the usage reporting plugin even if API key and graph ref are provided;
in this case, a warning is logged.

You still can explicitly install ApolloServerPluginUsageReporting in
subgraphs, though a warning will be logged.

Fixes #7121.

Also, update terminology around subgraphs. Various log messages and internal
symbol names and comments referred to "federated service" rather than the
newer and clearer "subgraph".

Patch:
diff --git a/.changeset/rude-steaks-smell.md b/.changeset/rude-steaks-smell.md
new file mode 100644
index 000000000..09cd5ddbc
--- /dev/null
+++ b/.changeset/rude-steaks-smell.md
@@ -0,0 +1,5 @@
+---
+'@apollo/server': minor
+---
+
+Don't automatically install the usage reporting plugin in servers that appear to be hosting a federated subgraph (based on the existence of a field `_Service.sdl: String`). This is generally a misconfiguration. If an API key and graph ref are provided to the subgraph, log a warning and do not enable the usage reporting plugin. If the usage reporting plugin is explicitly installed in a subgraph, log a warning but keep it enabled.
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/docs/source/api/plugin/usage-reporting.mdx b/docs/source/api/plugin/usage-reporting.mdx
index 45f703797..fd9ef6b0b 100644
--- a/docs/source/api/plugin/usage-reporting.mdx
+++ b/docs/source/api/plugin/usage-reporting.mdx
@@ -5,16 +5,20 @@ api_reference: true
 
 Apollo Server's built-in usage reporting plugin gathers data on how your clients use the operations and fields in your GraphQL schema. The plugin also handles pushing this usage data to [Apollo Studio](/studio/), as described in [Metrics and logging](../../monitoring/metrics/).
 
+This plugin is designed to be used in an Apollo Gateway or in a monolithic server; it is not designed to be used from a subgraph. In a supergraph running Apollo Federation, the Apollo Gateway or Apollo Router will send usage reports to Apollo's cloud. Subgraphs don't need to also send usage reports to Apollo's cloud; instead, they send it to the Router via [inline traces](./inline-trace/) and the Router combines execution information across all subgraphs and sends summarized reports to the cloud.
+
 ## Default installation
 
 > 📣 **New in Apollo Server 4**: error details are not included in traces by default. For more details, see [Error Handling](../../data/errors/#masking-and-logging-errors).
 
-Apollo Server automatically installs and enables this plugin with default settings if you [provide a graph API key and a graph ref to Apollo Server](../../monitoring/metrics/#connecting-to-apollo-studio). You usually do this by setting the `APOLLO_KEY` and `APOLLO_GRAPH_REF` (or `APOLLO_GRAPH_ID` and `APOLLO_GRAPH_VARIANT`) environment variables. No other action is required.
+Apollo Server automatically installs and enables this plugin with default settings if you [provide a graph API key and a graph ref to Apollo Server](../../monitoring/metrics/#connecting-to-apollo-studio) and your server is not a federated subgraph. You usually do this by setting the `APOLLO_KEY` and `APOLLO_GRAPH_REF` (or `APOLLO_GRAPH_ID` and `APOLLO_GRAPH_VARIANT`) environment variables. No other action is required.
 
-If you don't provide an API key and graph ref, this plugin is not installed.
+If you don't provide an API key and graph ref, or if your server is a federated subgraph, this plugin is not automatically installed.
 
 If you provide an API key but _don't_ provide a graph ref, a warning is logged. You can [disable the plugin](#disabling-the-plugin) to hide the warning.
 
+If you provide an API key and graph ref but your server _is_ a federated subgraph, a warning is logged. You can [disable the plugin](#disabling-the-plugin) to hide the warning.
+
 ## Custom installation
 
 If you want to configure the usage reporting plugin, import it and pass it to your `ApolloServer` constructor's `plugins` array:
@@ -38,6 +42,8 @@ const server = new ApolloServer({
 
 </MultiCodeBlock>
 
+> While you can install the usage reporting plugin in a server that is a federated subgraph, this is not recommended, and a warning will be logged.
+
 Supported configuration options are listed below.
 
 #### Options
@@ -529,4 +535,4 @@ const server = new ApolloServer({
 
 </MultiCodeBlock>
 
-This also disables the warning log if you provide an API key but do not provide a graph ref.
+This also disables the warning log if you provide an API key but do not provide a graph ref, or if you provide an API key and graph ref and your server is a federated subgraph.
diff --git a/docs/source/migration.mdx b/docs/source/migration.mdx
index b7165952a..7a96d06bf 100644
--- a/docs/source/migration.mdx
+++ b/docs/source/migration.mdx
@@ -1743,6 +1743,15 @@ new ApolloServer({
 
 (As [described above](#rewriteerror-plugin-option), the `rewriteError` option has been replaced by a `transform` option on `sendErrors` or `includeErrors`.)
 
+
+### Usage reporting plugin is off by default on subgraphs
+
+In an Apollo Federation supergraph, your Apollo Gateway or Apollo Router sends [usage reports](./api/plugin/usage-reporting/) to Apollo's servers; information about what happens inside individual subgraph servers is sent from the subgraphs to the Gateway or Router via [inline traces](./api/plugin/inline-trace/). That is to say: the usage reporting plugin is *not* designed for use in federated subgraphs.
+
+In Apollo Server 3, if you provide an Apollo API key and graph ref and do not explicitly install the `ApolloServerPluginUsageReporting` or `ApolloServerPluginUsageReportingDisabled` plugins, the `ApolloServerPluginUsageReporting` plugin will be installed with its default configuration, even if the server is a subgraph.
+
+In Apollo Server 4, this automatic installation does not occur in federated subgraphs. You still can explicitly install `ApolloServerPluginUsageReporting` in your subgraph, though this is not recommended and a warning will be logged.
+
 ## Renamed packages
 
 The following packages have been renamed in Apollo Server 4:
diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts
index 123de66aa..13f88e5f5 100644
--- a/packages/server/src/ApolloServer.ts
+++ b/packages/server/src/ApolloServer.ts
@@ -869,7 +869,11 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
           const { ApolloServerPluginUsageReporting } = await import(
             './plugin/usageReporting/index.js'
           );
-          plugins.unshift(ApolloServerPluginUsageReporting());
+          plugins.unshift(
+            ApolloServerPluginUsageReporting({
+              __onlyIfSchemaIsNotSubgraph: true,
+            }),
+          );
         } else {
           this.logger.warn(
             'You have specified an Apollo key but have not specified a graph ref; usage ' +
@@ -917,7 +921,7 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
           './plugin/inlineTrace/index.js'
         );
         plugins.push(
-          ApolloServerPluginInlineTrace({ __onlyIfSchemaIsFederated: true }),
+          ApolloServerPluginInlineTrace({ __onlyIfSchemaIsSubgraph: true }),
         );
       }
     }
diff --git a/packages/server/src/plugin/inlineTrace/index.ts b/packages/server/src/plugin/inlineTrace/index.ts
index fa2de0215..0c8ba0ac5 100644
--- a/packages/server/src/plugin/inlineTrace/index.ts
+++ b/packages/server/src/plugin/inlineTrace/index.ts
@@ -2,7 +2,7 @@ import { Trace } from '@apollo/usage-reporting-protobuf';
 import { TraceTreeBuilder } from '../traceTreeBuilder.js';
 import type { SendErrorsOptions } from '../usageReporting/index.js';
 import { internalPlugin } from '../../internalPlugin.js';
-import { schemaIsFederated } from '../schemaIsFederated.js';
+import { schemaIsSubgraph } from '../schemaIsSubgraph.js';
 import type { ApolloServerPlugin } from '../../externalTypes/index.js';
 
 export interface ApolloServerPluginInlineTraceOptions {
@@ -29,14 +29,14 @@ export interface ApolloServerPluginInlineTraceOptions {
   /**
    * This option is for internal use by `@apollo/server` only.
    *
-   * By default we want to enable this plugin for federated schemas only, but we
+   * By default we want to enable this plugin for subgraph schemas only, but we
    * need to come up with our list of plugins before we have necessarily loaded
    * the schema. So (unless the user installs this plugin or
-   * ApolloServerPluginInlineTraceDisabled themselves), `@apollo/server`
-   * always installs this plugin and uses this option to make sure traces are
-   * only included if the schema appears to be federated.
+   * ApolloServerPluginInlineTraceDisabled themselves), `@apollo/server` always
+   * installs this plugin and uses this option to make sure traces are only
+   * included if the schema appears to be a subgraph.
    */
-  __onlyIfSchemaIsFederated?: boolean;
+  __onlyIfSchemaIsSubgraph?: boolean;
 }
 
 // This ftv1 plugin produces a base64'd Trace protobuf containing only the
@@ -47,7 +47,7 @@ export interface ApolloServerPluginInlineTraceOptions {
 export function ApolloServerPluginInlineTrace(
   options: ApolloServerPluginInlineTraceOptions = Object.create(null),
 ): ApolloServerPlugin {
-  let enabled: boolean | null = options.__onlyIfSchemaIsFederated ? null : true;
+  let enabled: boolean | null = options.__onlyIfSchemaIsSubgraph ? null : true;
   return internalPlugin({
     __internal_plugin_id__: 'InlineTrace',
     __is_disabled_plugin__: false,
@@ -57,10 +57,10 @@ export function ApolloServerPluginInlineTrace(
       // like the log line, just install `ApolloServerPluginInlineTrace()` in
       // `plugins` yourself.
       if (enabled === null) {
-        enabled = schemaIsFederated(schema);
+        enabled = schemaIsSubgraph(schema);
         if (enabled) {
           logger.info(
-            'Enabling inline tracing for this federated service. To disable, use ' +
+            'Enabling inline tracing for this subgraph. To disable, use ' +
               'ApolloServerPluginInlineTraceDisabled.',
           );
         }
diff --git a/packages/server/src/plugin/schemaIsFederated.ts b/packages/server/src/plugin/schemaIsFederated.ts
deleted file mode 100644
index 61b3dda7d..000000000
--- a/packages/server/src/plugin/schemaIsFederated.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { GraphQLSchema, isObjectType, isScalarType } from 'graphql';
-
-// Returns true if it appears that the schema was returned from
-// @apollo/federation's buildFederatedSchema. This strategy avoids depending
-// explicitly on @apollo/federation or relying on something that might not
-// survive transformations like monkey-patching a boolean field onto the
-// schema.
-//
-// This is used for two things:
-// 1) Determining whether traces should be added to responses if requested
-//    with an HTTP header. If you want to include these traces even for
-//    non-federated schemas (when requested via header) you can use
-//    ApolloServerPluginInlineTrace yourself; if you want to never
-//    include these traces even for federated schemas you can use
-//    ApolloServerPluginInlineTraceDisabled.
-// 2) Determining whether schema-reporting should be allowed; federated
-//    services shouldn't be reporting schemas, and we accordingly throw if
-//    it's attempted.
-export function schemaIsFederated(schema: GraphQLSchema): boolean {
-  const serviceType = schema.getType('_Service');
-  if (!isObjectType(serviceType)) {
-    return false;
-  }
-  const sdlField = serviceType.getFields().sdl;
-  if (!sdlField) {
-    return false;
-  }
-  const sdlFieldType = sdlField.type;
-  if (!isScalarType(sdlFieldType)) {
-    return false;
-  }
-  return sdlFieldType.name == 'String';
-}
diff --git a/packages/server/src/plugin/schemaIsSubgraph.ts b/packages/server/src/plugin/schemaIsSubgraph.ts
new file mode 100644
index 000000000..88e75aecb
--- /dev/null
+++ b/packages/server/src/plugin/schemaIsSubgraph.ts
@@ -0,0 +1,32 @@
+import { GraphQLSchema, isObjectType, isScalarType } from 'graphql';
+
+// Returns true if it appears that the schema was appears to be of a subgraph
+// (eg, returned from @apollo/subgraph's buildSubgraphSchema). This strategy
+// avoids depending explicitly on @apollo/subgraph or relying on something that
+// might not survive transformations like monkey-patching a boolean field onto
+// the schema.
+//
+// This is used for two things:
+// 1) Determining whether traces should be added to responses if requested with
+//    an HTTP header. If you want to include these traces even for non-subgraphs
+//    (when requested via header, eg for Apollo Explorer's trace view) you can
+//    use ApolloServerPluginInlineTrace explicitly; if you want to never include
+//    these traces even for subgraphs you can use
+//    ApolloServerPluginInlineTraceDisabled.
+// 2) Determining whether schema-reporting should be allowed; subgraphs cannot
+//    report schemas, and we accordingly throw if it's attempted.
+export function schemaIsSubgraph(schema: GraphQLSchema): boolean {
+  const serviceType = schema.getType('_Service');
+  if (!isObjectType(serviceType)) {
+    return false;
+  }
+  const sdlField = serviceType.getFields().sdl;
+  if (!sdlField) {
+    return false;
+  }
+  const sdlFieldType = sdlField.type;
+  if (!isScalarType(sdlFieldType)) {
+    return false;
+  }
+  return sdlFieldType.name == 'String';
+}
diff --git a/packages/server/src/plugin/schemaReporting/index.ts b/packages/server/src/plugin/schemaReporting/index.ts
index 3ff24e86c..780e32ba8 100644
--- a/packages/server/src/plugin/schemaReporting/index.ts
+++ b/packages/server/src/plugin/schemaReporting/index.ts
@@ -3,7 +3,7 @@ import { internalPlugin } from '../../internalPlugin.js';
 import { v4 as uuidv4 } from 'uuid';
 import { printSchema, validateSchema, buildSchema } from 'graphql';
 import { SchemaReporter } from './schemaReporter.js';
-import { schemaIsFederated } from '../schemaIsFederated.js';
+import { schemaIsSubgraph } from '../schemaIsSubgraph.js';
 import type { SchemaReport } from './generated/operations.js';
 import type { ApolloServerPlugin } from '../../externalTypes/index.js';
 import type { Fetcher } from '@apollo/utils.fetcher';
@@ -108,12 +108,12 @@ export function ApolloServerPluginSchemaReporting(
         }
       }
 
-      if (schemaIsFederated(schema)) {
+      if (schemaIsSubgraph(schema)) {
         throw Error(
           [
-            'Schema reporting is not yet compatible with federated services.',
-            "If you're interested in using schema reporting with federated",
-            'services, please contact Apollo support. To set up managed federation, see',
+            'Schema reporting is not yet compatible with Apollo Federation subgraphs.',
+            "If you're interested in using schema reporting with subgraphs,",
+            'please contact Apollo support. To set up managed federation, see',
             'https://go.apollo.dev/s/managed-federation',
           ].join(' '),
         );
diff --git a/packages/server/src/plugin/usageReporting/options.ts b/packages/server/src/plugin/usageReporting/options.ts
index 6936bc505..62b7ff7d6 100644
--- a/packages/server/src/plugin/usageReporting/options.ts
+++ b/packages/server/src/plugin/usageReporting/options.ts
@@ -367,6 +367,17 @@ export interface ApolloServerPluginUsageReportingOptions<
    * about how the signature relates to the operation you executed.
    */
   calculateSignature?: (ast: DocumentNode, operationName: string) => string;
+  /**
+   * This option is for internal use by `@apollo/server` only.
+   *
+   * By default we want to enable this plugin for non-subgraph schemas only, but
+   * we need to come up with our list of plugins before we have necessarily
+   * loaded the schema. So (unless the user installs this plugin or
+   * ApolloServerPluginUsageReportingDisabled themselves), `@apollo/server`
+   * always installs this plugin (if API key and graph ref are provided) and
+   * uses this option to disable usage reporting if the schema is a subgraph.
+   */
+  __onlyIfSchemaIsNotSubgraph?: boolean;
   //#endregion
 }
 
diff --git a/packages/server/src/plugin/usageReporting/plugin.ts b/packages/server/src/plugin/usageReporting/plugin.ts
index 1461b16d0..6f4e25699 100644
--- a/packages/server/src/plugin/usageReporting/plugin.ts
+++ b/packages/server/src/plugin/usageReporting/plugin.ts
@@ -39,6 +39,7 @@ import { makeTraceDetails } from './traceDetails.js';
 import { packageVersion } from '../../generated/packageVersion.js';
 import { computeCoreSchemaHash } from '../../utils/computeCoreSchemaHash.js';
 import type { HeaderMap } from '../../utils/HeaderMap.js';
+import { schemaIsSubgraph } from '../schemaIsSubgraph.js';
 
 const gzipPromise = promisify(gzip);
 
@@ -70,9 +71,11 @@ export function ApolloServerPluginUsageReporting<TContext extends BaseContext>(
       ? fieldLevelInstrumentationOption
       : async () => true;
 
-  let requestDidStartHandler: (
-    requestContext: GraphQLRequestContext<TContext>,
-  ) => GraphQLRequestListener<TContext>;
+  let requestDidStartHandler:
+    | ((
+        requestContext: GraphQLRequestContext<TContext>,
+      ) => GraphQLRequestListener<TContext>)
+    | null = null;
   return internalPlugin({
     __internal_plugin_id__: 'UsageReporting',
     __is_disabled_plugin__: false,
@@ -81,20 +84,19 @@ export function ApolloServerPluginUsageReporting<TContext extends BaseContext>(
     // this little hack. (Perhaps we should also allow GraphQLServerListener to contain
     // a requestDidStart?)
     async requestDidStart(requestContext: GraphQLRequestContext<TContext>) {
-      if (!requestDidStartHandler) {
-        throw Error(
-          'The usage reporting plugin has been asked to handle a request before the ' +
-            'server has started. See https://github.com/apollographql/apollo-server/issues/4588 ' +
-            'for more details.',
-        );
+      if (requestDidStartHandler) {
+        return requestDidStartHandler(requestContext);
       }
-      return requestDidStartHandler(requestContext);
+      // This happens if usage reporting is disabled (eg because this is a
+      // subgraph).
+      return {};
     },
 
     async serverWillStart({
       logger: serverLogger,
       apollo,
       startedInBackground,
+      schema,
     }): Promise<GraphQLServerListener> {
       // Use the plugin-specific logger if one is provided; otherwise the general server one.
       const logger = options.logger ?? serverLogger;
@@ -108,6 +110,31 @@ export function ApolloServerPluginUsageReporting<TContext extends BaseContext>(
         );
       }
 
+      if (schemaIsSubgraph(schema)) {
+        if (options.__onlyIfSchemaIsNotSubgraph) {
+          logger.warn(
+            'You have specified an Apollo API key and graph ref but this server appears ' +
+              'to be a subgraph. Typically usage reports are sent to Apollo by your Router ' +
+              'or Gateway, not directly from your subgraph; usage reporting is disabled. To ' +
+              'enable usage reporting anyway, explicitly install `ApolloServerPluginUsageReporting`. ' +
+              'To disable this warning, install `ApolloServerPluginUsageReportingDisabled`.',
+          );
+          // This early return means we don't start background timers, don't
+          // register serverDidStart, don't assign requestDidStartHandler, etc.
+          return {};
+        } else {
+          // This is just a warning; usage reporting is still enabled. If it
+          // turns out there are lots of people who really need to have this odd
+          // setup and they don't like the warning, we can provide a new option
+          // to disable the warning (or they can filter in their `logger`).
+          logger.warn(
+            'You have installed `ApolloServerPluginUsageReporting` but this server appears to ' +
+              'be a subgraph. Typically usage reports are sent to Apollo by your Router ' +
+              'or Gateway, not directly from your subgraph.',
+          );
+        }
+      }
+
       logger.info(
         'Apollo usage reporting starting! See your graph at ' +
           `https://studio.apollographql.com/graph/${encodeURI(graphRef)}/`,
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', () => {