diff --git a/.changeset/eight-bats-shop.md b/.changeset/eight-bats-shop.md
new file mode 100644
index 00000000000..7d9e0a86832
--- /dev/null
+++ b/.changeset/eight-bats-shop.md
@@ -0,0 +1,5 @@
+---
+'@apollo/server': minor
+---
+
+New `ApolloServerPluginSchemaReportingDisabled` plugin which can override the `APOLLO_SCHEMA_REPORTING` environment variable.
diff --git a/.changeset/strong-laws-bow.md b/.changeset/strong-laws-bow.md
new file mode 100644
index 00000000000..f7aac153fb7
--- /dev/null
+++ b/.changeset/strong-laws-bow.md
@@ -0,0 +1,5 @@
+---
+'@apollo/server': minor
+---
+
+It is now an error to combine a "disabled" plugin such as `ApolloServerPluginUsageReportingDisabled` with its enabled counterpart such as `ApolloServerPluginUsageReporting`.
diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx
index 1f019931e26..53e6d5d2d03 100644
--- a/docs/source/api/apollo-server.mdx
+++ b/docs/source/api/apollo-server.mdx
@@ -372,10 +372,10 @@ To learn more about configuring Apollo Server's cache, see [Configuring cache ba
An array of [plugins](../integrations/plugins) to install in your server instance. Each array element can be either a valid plugin object or a zero-argument function that _returns_ a valid plugin object.
-In certain cases, Apollo Server installs some of its built-in plugins automatically (for example, when you provide an Apollo Studio API key with the `APOLLO_KEY` environment variable). For details, see the API references for these plugins: [usage reporting](./plugin/usage-reporting/), [schema reporting](./plugin/schema-reporting/), [landing page](./plugin/landing-pages/), and [inline trace](./plugin/inline-trace/).
-
You can _also_ add plugins to your server before you start it using the [`addPlugin`](#addplugin) method.
+Apollo Server comes with several plugins that it installs automatically in their default configuration if certain conditions are met. For example, the usage reporting plugin is installed if you provide a graph API key and a graph ref. Apollo Server skips this automatic installation if you manually provide the plugin (in the `plugins` array or with the `addPlugin` method), or if you provide the plugin's corresponding "disabled" plugin (such as `ApolloServerPluginUsageReportingDisabled()` for `ApolloServerPluginUsageReporting`). For details, see the API references for these plugins: [usage reporting](./plugin/usage-reporting/), [schema reporting](./plugin/schema-reporting/), [landing page](./plugin/landing-pages/), [cache control](./plugin/cache-control/), and [inline trace](./plugin/inline-trace/).
+
diff --git a/docs/source/api/plugin/schema-reporting.mdx b/docs/source/api/plugin/schema-reporting.mdx
index 5c71cccf123..39d42a0b620 100644
--- a/docs/source/api/plugin/schema-reporting.mdx
+++ b/docs/source/api/plugin/schema-reporting.mdx
@@ -122,3 +122,24 @@ Specifies which [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fet
+
+## Disabling the plugin
+
+The schema reporting plugin is only automatically installed if the `APOLLO_SCHEMA_REPORTING` is set to `true`, so the easiest way to disable the plugin is to avoid setting that environment variable (and to not explicitly install `ApolloServerPluginSchemaReporting`). However, if you'd like to ensure that the schema reporting plugin is not installed in your server (perhaps for a test that might run with arbitrary environment variables set), you can disable schema reporting by installing the `ApolloServerPluginSchemaReportingDisabled` plugin, like so:
+
+
+
+```ts
+import { ApolloServer } from '@apollo/server';
+import { ApolloServerPluginSchemaReportingDisabled } from '@apollo/server/plugin/disabled';
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+ plugins: [
+ ApolloServerPluginSchemaReportingDisabled(),
+ ],
+});
+```
+
+
diff --git a/docs/source/migration.mdx b/docs/source/migration.mdx
index 66c02d2784b..9e519f06c0b 100644
--- a/docs/source/migration.mdx
+++ b/docs/source/migration.mdx
@@ -1429,6 +1429,29 @@ new ApolloServer({
});
```
+## "Disabled" plugins cannot be combined with their enabled counterpart
+
+Apollo Server has several plugins that are installed by default in certain conditions. To override this behavior, each of these plugins has a "disabled" counterpart that prevents this default installation.
+
+But what happens if you combine the manual installation of a plugin with its disabled counterpart? Consider the following code:
+
+```ts
+const server = new ApolloServer({
+ schema,
+ plugins: [
+ ApolloServerPluginUsageReporting(),
+ ApolloServerPluginUsageReportingDisabled(),
+ ]
+});
+await server.start();
+```
+
+In Apollo Server 3, the "disabled" plugin is simply ignored if combined with its enabled counterpart. This could lead to confusion, as it can appear that an attempt to disable a feature is completely ignored.
+
+In Apollo Server 4, `await server.start()` will throw if you combine a "disabled" plugin with its enabled counterpart, with an error like `You have tried to install both ApolloServerPluginUsageReporting and ApolloServerPluginUsageReportingDisabled`. If your server throws this error, choose whether you want the feature enabled or disabled, and install only the appropriate plugin.
+
+(This change affects the usage reporting, inline trace, and cache control features. It also affects the schema reporting feature, although `ApolloServerPluginSchemaReportingDisabled` did not exist in Apollo Server 3. For technical reasons, it does not affect the landing page feature: combining `ApolloServerPluginLandingPageDisabled` with a landing page plugin should be considered as unspecified behavior which may change in a future release of Apollo Server 4.)
+
## Plugin API changes
### Fields on `GraphQLRequestContext`
diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts
index 8bfb9fc465c..3911cc5ae95 100644
--- a/packages/server/src/ApolloServer.ts
+++ b/packages/server/src/ApolloServer.ts
@@ -804,9 +804,48 @@ export class ApolloServer {
const alreadyHavePluginWithInternalId = (id: InternalPluginId) =>
plugins.some(
- (p) => pluginIsInternal(p) && p.__internal_plugin_id__() === id,
+ (p) => pluginIsInternal(p) && p.__internal_plugin_id__ === id,
);
+ // Make sure we're not trying to explicitly enable and disable the same
+ // feature. (Be careful: we are not trying to stop people from installing
+ // the same plugin twice if they have a use case for it, like two usage
+ // reporting plugins for different variants.)
+ //
+ // Note that this check doesn't work for the landing page plugin, because
+ // users can write their own landing page plugins and we don't know if a
+ // given plugin is a landing page plugin until the server has started.
+ const pluginsByInternalID = new Map<
+ InternalPluginId,
+ { sawDisabled: boolean; sawNonDisabled: boolean }
+ >();
+ for (const p of plugins) {
+ if (pluginIsInternal(p)) {
+ const id = p.__internal_plugin_id__;
+ if (!pluginsByInternalID.has(id)) {
+ pluginsByInternalID.set(id, {
+ sawDisabled: false,
+ sawNonDisabled: false,
+ });
+ }
+ const seen = pluginsByInternalID.get(id)!;
+ if (p.__is_disabled_plugin__) {
+ seen.sawDisabled = true;
+ } else {
+ seen.sawNonDisabled = true;
+ }
+
+ if (seen.sawDisabled && seen.sawNonDisabled) {
+ throw new Error(
+ `You have tried to install both ApolloServerPlugin${id} and ` +
+ `ApolloServerPlugin${id}Disabled in your server. Please choose ` +
+ `whether or not you want to disable the feature and install the ` +
+ `appropriate plugin for your use case.`,
+ );
+ }
+ }
+ }
+
// Special case: cache control is on unless you explicitly disable it.
{
if (!alreadyHavePluginWithInternalId('CacheControl')) {
diff --git a/packages/server/src/__tests__/plugin/usageReporting/plugin.test.ts b/packages/server/src/__tests__/plugin/usageReporting/plugin.test.ts
index 6f16ad3a7f8..f6ed6a86f14 100644
--- a/packages/server/src/__tests__/plugin/usageReporting/plugin.test.ts
+++ b/packages/server/src/__tests__/plugin/usageReporting/plugin.test.ts
@@ -24,7 +24,10 @@ import {
ApolloServerPluginUsageReportingOptions,
ApolloServerPluginUsageReporting,
} from '../../../plugin/usageReporting';
-import { ApolloServerPluginCacheControlDisabled } from '../../../plugin/disabled';
+import {
+ ApolloServerPluginCacheControlDisabled,
+ ApolloServerPluginUsageReportingDisabled,
+} from '../../../plugin/disabled';
import { describe, it, expect, afterEach } from '@jest/globals';
const quietLogger = loglevel.getLogger('quiet');
@@ -525,3 +528,19 @@ describe('sendHeaders makeHTTPRequestHeaders helper', () => {
expect(http.requestHeaders['set-cookie']).toBe(undefined);
});
});
+
+it('cannot combine enabling with disabling', async () => {
+ const server = new ApolloServer({
+ typeDefs: 'type Query { x: ID }',
+ plugins: [
+ ApolloServerPluginUsageReporting(),
+ ApolloServerPluginUsageReportingDisabled(),
+ ],
+ });
+ await expect(server.start()).rejects.toThrow(
+ 'You have tried to install both ApolloServerPluginUsageReporting ' +
+ 'and ApolloServerPluginUsageReportingDisabled in your server. Please ' +
+ 'choose whether or not you want to disable the feature and install the ' +
+ 'appropriate plugin for your use case.',
+ );
+});
diff --git a/packages/server/src/internalPlugin.ts b/packages/server/src/internalPlugin.ts
index 410c560c946..206f6a32671 100644
--- a/packages/server/src/internalPlugin.ts
+++ b/packages/server/src/internalPlugin.ts
@@ -11,7 +11,8 @@ export interface InternalApolloServerPlugin
extends ApolloServerPlugin {
// Used to identify a few specific plugins that are instantiated
// by default if not explicitly used or disabled.
- __internal_plugin_id__(): InternalPluginId;
+ __internal_plugin_id__: InternalPluginId;
+ __is_disabled_plugin__: boolean;
}
// Helper function for writing internal plugins which lets you write an object
diff --git a/packages/server/src/plugin/cacheControl/index.ts b/packages/server/src/plugin/cacheControl/index.ts
index f80231dc223..8462e66f573 100644
--- a/packages/server/src/plugin/cacheControl/index.ts
+++ b/packages/server/src/plugin/cacheControl/index.ts
@@ -63,9 +63,8 @@ export function ApolloServerPluginCacheControl(
>;
return internalPlugin({
- __internal_plugin_id__() {
- return 'CacheControl';
- },
+ __internal_plugin_id__: 'CacheControl',
+ __is_disabled_plugin__: false,
async serverWillStart({ schema }) {
// Set the size of the caches to be equal to the number of composite types
diff --git a/packages/server/src/plugin/disabled/index.ts b/packages/server/src/plugin/disabled/index.ts
index 85adee10b90..b427fef6d90 100644
--- a/packages/server/src/plugin/disabled/index.ts
+++ b/packages/server/src/plugin/disabled/index.ts
@@ -14,9 +14,8 @@ import type {
function disabledPlugin(id: InternalPluginId): ApolloServerPlugin {
const plugin: InternalApolloServerPlugin = {
- __internal_plugin_id__() {
- return id;
- },
+ __internal_plugin_id__: id,
+ __is_disabled_plugin__: true,
};
return plugin;
}
@@ -33,6 +32,10 @@ export function ApolloServerPluginLandingPageDisabled(): ApolloServerPlugin {
+ return disabledPlugin('SchemaReporting');
+}
+
export function ApolloServerPluginUsageReportingDisabled(): ApolloServerPlugin {
return disabledPlugin('UsageReporting');
}
diff --git a/packages/server/src/plugin/inlineTrace/index.ts b/packages/server/src/plugin/inlineTrace/index.ts
index 2977fd2a31e..fa2de0215b8 100644
--- a/packages/server/src/plugin/inlineTrace/index.ts
+++ b/packages/server/src/plugin/inlineTrace/index.ts
@@ -49,9 +49,8 @@ export function ApolloServerPluginInlineTrace(
): ApolloServerPlugin {
let enabled: boolean | null = options.__onlyIfSchemaIsFederated ? null : true;
return internalPlugin({
- __internal_plugin_id__() {
- return 'InlineTrace';
- },
+ __internal_plugin_id__: 'InlineTrace',
+ __is_disabled_plugin__: false,
async serverWillStart({ schema, logger }) {
// Handle the case that the plugin was implicitly installed. We only want it
// to actually be active if the schema appears to be federated. If you don't
diff --git a/packages/server/src/plugin/schemaReporting/index.ts b/packages/server/src/plugin/schemaReporting/index.ts
index d895740674d..3ff24e86ccd 100644
--- a/packages/server/src/plugin/schemaReporting/index.ts
+++ b/packages/server/src/plugin/schemaReporting/index.ts
@@ -69,9 +69,8 @@ export function ApolloServerPluginSchemaReporting(
const bootId = uuidv4();
return internalPlugin({
- __internal_plugin_id__() {
- return 'SchemaReporting';
- },
+ __internal_plugin_id__: 'SchemaReporting',
+ __is_disabled_plugin__: false,
async serverWillStart({ apollo, schema, logger }) {
const { key, graphRef } = apollo;
if (!key) {
diff --git a/packages/server/src/plugin/usageReporting/plugin.ts b/packages/server/src/plugin/usageReporting/plugin.ts
index 213561918f6..02c4962635a 100644
--- a/packages/server/src/plugin/usageReporting/plugin.ts
+++ b/packages/server/src/plugin/usageReporting/plugin.ts
@@ -70,9 +70,8 @@ export function ApolloServerPluginUsageReporting(
requestContext: GraphQLRequestContext,
) => GraphQLRequestListener;
return internalPlugin({
- __internal_plugin_id__() {
- return 'UsageReporting';
- },
+ __internal_plugin_id__: 'UsageReporting',
+ __is_disabled_plugin__: false,
// We want to be able to access locals from `serverWillStart` in our `requestDidStart`, thus
// this little hack. (Perhaps we should also allow GraphQLServerListener to contain