UNPKG

@apollo/server

Version:
1,298 lines (1,211 loc) 55.1 kB
import type { GatewayExecutor } from '@apollo/server-gateway-interface'; import { isNodeLike } from '@apollo/utils.isnodelike'; import { InMemoryLRUCache, PrefixingKeyValueCache, type KeyValueCache, } from '@apollo/utils.keyvaluecache'; import type { Logger } from '@apollo/utils.logger'; import type { WithRequired } from '@apollo/utils.withrequired'; import { makeExecutableSchema } from '@graphql-tools/schema'; import resolvable, { type Resolvable } from './utils/resolvable.js'; import { GraphQLError, assertValidSchema, print, printSchema, type DocumentNode, type FormattedExecutionResult, type GraphQLFieldResolver, type GraphQLFormattedError, type GraphQLSchema, type ParseOptions, type TypedQueryDocumentNode, type ValidationRule, } from 'graphql'; import loglevel from 'loglevel'; import Negotiator from 'negotiator'; import { newCachePolicy } from './cachePolicy.js'; import { determineApolloConfig } from './determineApolloConfig.js'; import { ensureError, ensureGraphQLError, normalizeAndFormatErrors, } from './errorNormalize.js'; import { ApolloServerErrorCode } from './errors/index.js'; import type { ApolloServerOptionsWithStaticSchema } from './externalTypes/constructor.js'; import type { ExecuteOperationOptions, VariableValues, } from './externalTypes/graphql.js'; import type { ApolloConfig, ApolloServerOptions, ApolloServerPlugin, BaseContext, ContextThunk, DocumentStore, GraphQLRequest, GraphQLRequestContext, GraphQLResponse, GraphQLServerContext, GraphQLServerListener, HTTPGraphQLHead, HTTPGraphQLRequest, HTTPGraphQLResponse, LandingPage, PersistedQueryOptions, } from './externalTypes/index.js'; import { runPotentiallyBatchedHttpQuery } from './httpBatching.js'; import type { GraphQLExperimentalIncrementalExecutionResults } from './incrementalDeliveryPolyfill.js'; import { pluginIsInternal, type InternalPluginId } from './internalPlugin.js'; import { preventCsrf, recommendedCsrfPreventionRequestHeaders, } from './preventCsrf.js'; import { APQ_CACHE_PREFIX, processGraphQLRequest } from './requestPipeline.js'; import { newHTTPGraphQLHead, prettyJSONStringify } from './runHttpQuery.js'; import { HeaderMap } from './utils/HeaderMap.js'; import { UnreachableCaseError } from './utils/UnreachableCaseError.js'; import { computeCoreSchemaHash } from './utils/computeCoreSchemaHash.js'; import { isDefined } from './utils/isDefined.js'; import { SchemaManager } from './utils/schemaManager.js'; import { NoIntrospection, createMaxRecursiveSelectionsRule, DEFAULT_MAX_RECURSIVE_SELECTIONS, } from './validationRules/index.js'; export type SchemaDerivedData = { schema: GraphQLSchema; // A store that, when enabled (default), will store the parsed and validated // versions of operations in-memory, allowing subsequent parses/validates // on the same operation to be executed immediately. documentStore: DocumentStore | null; // Prefix for keys in the DocumentStore if a custom one is provided (to // separate the cache for different schema versions). This is vital to // security so we do it explicitly so that // PrefixingKeyValueCache.cacheDangerouslyDoesNotNeedPrefixesForIsolation // doesn't affect it. documentStoreKeyPrefix: string; }; type RunningServerState = { schemaManager: SchemaManager; landingPage: LandingPage | null; }; type ServerState = | { phase: 'initialized'; schemaManager: SchemaManager; } | { phase: 'starting'; barrier: Resolvable<void>; schemaManager: SchemaManager; // This is set to true if you called // startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests // instead of start. The main purpose is that assertStarted allows you to // still be in the starting phase if this is set. (This is the serverless // use case.) startedInBackground: boolean; } | { phase: 'failed to start'; error: Error; } | ({ phase: 'started'; drainServers: (() => Promise<void>) | null; toDispose: (() => Promise<void>)[]; toDisposeLast: (() => Promise<void>)[]; } & RunningServerState) | ({ phase: 'draining'; barrier: Resolvable<void>; } & RunningServerState) | { phase: 'stopping'; barrier: Resolvable<void>; } | { phase: 'stopped'; stopError: Error | null; }; export interface ApolloServerInternals<TContext extends BaseContext> { state: ServerState; gatewayExecutor: GatewayExecutor | null; dangerouslyDisableValidation?: boolean; formatError?: ( formattedError: GraphQLFormattedError, error: unknown, ) => GraphQLFormattedError; includeStacktraceInErrorResponses: boolean; persistedQueries?: WithRequired<PersistedQueryOptions, 'cache'>; nodeEnv: string; allowBatchedHttpRequests: boolean; apolloConfig: ApolloConfig; plugins: ApolloServerPlugin<TContext>[]; parseOptions: ParseOptions; // `undefined` means we figure out what to do during _start (because // the default depends on whether or not we used the background version // of start). stopOnTerminationSignals: boolean | undefined; csrfPreventionRequestHeaders: string[] | null; rootValue?: ((parsedQuery: DocumentNode) => unknown) | unknown; validationRules: Array<ValidationRule>; laterValidationRules?: Array<ValidationRule>; hideSchemaDetailsFromClientErrors: boolean; fieldResolver?: GraphQLFieldResolver<any, TContext>; // TODO(AS6): remove this option. status400ForVariableCoercionErrors?: boolean; __testing_incrementalExecutionResults?: GraphQLExperimentalIncrementalExecutionResults; stringifyResult: ( value: FormattedExecutionResult, ) => string | Promise<string>; } function defaultLogger(): Logger { const loglevelLogger = loglevel.getLogger('apollo-server'); loglevelLogger.setLevel(loglevel.levels.INFO); return loglevelLogger; } // We really want to prevent this from being legal: // // const s: ApolloServer<{}> = // new ApolloServer<{importantContextField: boolean}>({ ... }); // s.executeOperation({query}, {contextValue: {}}); // // ie, if you declare an ApolloServer whose context values must be of a certain // type, you can't assign it to a variable whose context values are less // constrained and then pass in a context value missing important fields. // // We also want this to be illegal: // // const sBase = new ApolloServer<{}>({ ... }); // const s: ApolloServer<{importantContextField: boolean}> = sBase; // s.addPlugin({async requestDidStart({contextValue: {importantContextField}}) { ... }}) // sBase.executeOperation({query}, {contextValue: {}}); // // so you shouldn't be able to assign an ApolloServer to a variable whose // context values are more constrained, either. So we want to declare that // ApolloServer is *invariant* in TContext, which we do with `in out` (a // TypeScript 4.7 feature). export class ApolloServer<in out TContext extends BaseContext = BaseContext> { private internals: ApolloServerInternals<TContext>; public readonly cache: KeyValueCache<string>; public readonly logger: Logger; constructor(config: ApolloServerOptions<TContext>) { const nodeEnv = config.nodeEnv ?? process.env.NODE_ENV ?? ''; this.logger = config.logger ?? defaultLogger(); const apolloConfig = determineApolloConfig(config.apollo, this.logger); const isDev = nodeEnv !== 'production'; if ( config.cache && config.cache !== 'bounded' && PrefixingKeyValueCache.prefixesAreUnnecessaryForIsolation(config.cache) ) { throw new Error( 'You cannot pass a cache returned from ' + '`PrefixingKeyValueCache.cacheDangerouslyDoesNotNeedPrefixesForIsolation`' + 'to `new ApolloServer({ cache })`, because Apollo Server may use it for ' + 'multiple features whose cache keys must be distinct from each other.', ); } const state: ServerState = config.gateway ? // ApolloServer has been initialized but we have not yet tried to load the // schema from the gateway. That will wait until `start()` or // `startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests()` // is called. (These may be called by other helpers; for example, // `standaloneServer` calls `start` for you inside its `listen` method, // and a serverless framework integration would call // startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests // for you.) { phase: 'initialized', schemaManager: new SchemaManager({ gateway: config.gateway, apolloConfig, schemaDerivedDataProvider: (schema) => ApolloServer.generateSchemaDerivedData( schema, config.documentStore, ), logger: this.logger, }), } : // We construct the schema synchronously so that we can fail fast if the // schema can't be constructed. (This used to be more important because we // used to have a 'schema' field that was publicly accessible immediately // after construction, though that field never actually worked with // gateways.) { phase: 'initialized', schemaManager: new SchemaManager({ apiSchema: ApolloServer.constructSchema(config), schemaDerivedDataProvider: (schema) => ApolloServer.generateSchemaDerivedData( schema, config.documentStore, ), logger: this.logger, }), }; const introspectionEnabled = config.introspection ?? isDev; const hideSchemaDetailsFromClientErrors = config.hideSchemaDetailsFromClientErrors ?? false; // We continue to allow 'bounded' for backwards-compatibility with the AS3.9 // API. this.cache = config.cache === undefined || config.cache === 'bounded' ? new InMemoryLRUCache() : config.cache; // Check whether the recursive selections limit has been enabled (off by // default), or whether a custom limit has been specified. const maxRecursiveSelectionsRule = config.maxRecursiveSelections === true ? [createMaxRecursiveSelectionsRule(DEFAULT_MAX_RECURSIVE_SELECTIONS)] : typeof config.maxRecursiveSelections === 'number' ? [createMaxRecursiveSelectionsRule(config.maxRecursiveSelections)] : []; // If the recursive selections rule has been enabled, then run configured // validations in a later validate() pass. const validationRules = [ ...(introspectionEnabled ? [] : [NoIntrospection]), ...maxRecursiveSelectionsRule, ]; let laterValidationRules; if (maxRecursiveSelectionsRule.length > 0) { laterValidationRules = config.validationRules; } else { validationRules.push(...(config.validationRules ?? [])); } // Note that we avoid calling methods on `this` before `this.internals` is assigned // (thus a bunch of things being static methods above). this.internals = { formatError: config.formatError, rootValue: config.rootValue, validationRules, laterValidationRules, hideSchemaDetailsFromClientErrors, dangerouslyDisableValidation: config.dangerouslyDisableValidation ?? false, fieldResolver: config.fieldResolver, includeStacktraceInErrorResponses: config.includeStacktraceInErrorResponses ?? (nodeEnv !== 'production' && nodeEnv !== 'test'), persistedQueries: config.persistedQueries === false ? undefined : { ...config.persistedQueries, cache: new PrefixingKeyValueCache( config.persistedQueries?.cache ?? this.cache, APQ_CACHE_PREFIX, ), }, nodeEnv, allowBatchedHttpRequests: config.allowBatchedHttpRequests ?? false, apolloConfig, // Note that more plugins can be added before `start()` with `addPlugin()` // (eg, plugins that want to take this ApolloServer as an argument), and // `start()` will call `addDefaultPlugins` to add default plugins. plugins: config.plugins ?? [], parseOptions: config.parseOptions ?? {}, state, stopOnTerminationSignals: config.stopOnTerminationSignals, gatewayExecutor: null, // set by _start csrfPreventionRequestHeaders: config.csrfPrevention === true || config.csrfPrevention === undefined ? recommendedCsrfPreventionRequestHeaders : config.csrfPrevention === false ? null : (config.csrfPrevention.requestHeaders ?? recommendedCsrfPreventionRequestHeaders), status400ForVariableCoercionErrors: config.status400ForVariableCoercionErrors ?? true, __testing_incrementalExecutionResults: config.__testing_incrementalExecutionResults, stringifyResult: config.stringifyResult ?? prettyJSONStringify, }; this.warnAgainstDeprecatedConfigOptions(config); } private warnAgainstDeprecatedConfigOptions( config: ApolloServerOptions<TContext>, ) { // TODO(AS6): this option goes away altogether. We should either update or remove this warning. if ('status400ForVariableCoercionErrors' in config) { if (config.status400ForVariableCoercionErrors === true) { this.logger.warn( 'The `status400ForVariableCoercionErrors: true` configuration option is now the default behavior and has no effect in Apollo Server v5. You can safely remove this option from your configuration.', ); } else { this.logger.warn( 'The `status400ForVariableCoercionErrors: false` configuration option is deprecated and will be removed in Apollo Server v6. Apollo recommends removing any dependency on this behavior.', ); } } } // Awaiting a call to `start` ensures that a schema has been loaded and that // all plugin `serverWillStart` hooks have been called. If either of these // processes throw, `start` will (async) throw as well. // // If you're using `standaloneServer`, you don't need to call `start` yourself // (in fact, it will throw if you do so); its `listen` method takes care of // that for you. // // If instead you're using an integration package for a non-serverless // framework (like Express), you must await a call to `start` immediately // after creating your `ApolloServer`, before attaching it to your web // framework and starting to accept requests. `start` should only be called // once; if it throws and you'd like to retry, just create another // `ApolloServer`. (Calling `start` was optional in Apollo Server 2, but in // Apollo Server 3+ the functions like `expressMiddleware` use `assertStarted` // to throw if `start` hasn't successfully completed.) // // Serverless integrations like Lambda do not support calling `start()`, // because their lifecycle doesn't allow you to wait before assigning a // handler or allowing the handler to be called. So they call // `startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests()` // instead, and don't really differentiate between startup failures and // request failures. This is hopefully appropriate for a "serverless" // framework. Serverless startup failures result in returning a redacted error // to the end user and logging the more detailed error. public async start(): Promise<void> { return await this._start(false); } public startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests(): void { this._start(true).catch((e) => this.logStartupError(e)); } private async _start(startedInBackground: boolean): Promise<void> { if (this.internals.state.phase !== 'initialized') { // If we wanted we could make this error detectable and change // `standaloneServer` to change the message to say not to call start() at // all. throw new Error( `You should only call 'start()' or ` + `'startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests()' ` + `once on your ApolloServer.`, ); } const schemaManager = this.internals.state.schemaManager; const barrier = resolvable(); this.internals.state = { phase: 'starting', barrier, schemaManager, startedInBackground, }; try { // Now that you can't call addPlugin any more, add default plugins like // usage reporting if they're not already added. await this.addDefaultPlugins(); const toDispose: (() => Promise<void>)[] = []; const executor = await schemaManager.start(); if (executor) { this.internals.gatewayExecutor = executor; } toDispose.push(async () => { await schemaManager.stop(); }); const schemaDerivedData = schemaManager.getSchemaDerivedData(); const service: GraphQLServerContext = { logger: this.logger, cache: this.cache, schema: schemaDerivedData.schema, apollo: this.internals.apolloConfig, startedInBackground, }; const taggedServerListeners = ( await Promise.all( this.internals.plugins.map(async (plugin) => ({ serverListener: plugin.serverWillStart && (await plugin.serverWillStart(service)), installedImplicitly: isImplicitlyInstallablePlugin(plugin) && plugin.__internal_installed_implicitly__, })), ) ).filter( ( maybeTaggedServerListener, ): maybeTaggedServerListener is { serverListener: GraphQLServerListener; installedImplicitly: boolean; } => typeof maybeTaggedServerListener.serverListener === 'object', ); taggedServerListeners.forEach( ({ serverListener: { schemaDidLoadOrUpdate } }) => { if (schemaDidLoadOrUpdate) { schemaManager.onSchemaLoadOrUpdate(schemaDidLoadOrUpdate); } }, ); const serverWillStops = taggedServerListeners .map((l) => l.serverListener.serverWillStop) .filter(isDefined); if (serverWillStops.length) { toDispose.push(async () => { await Promise.all( serverWillStops.map((serverWillStop) => serverWillStop()), ); }); } const drainServerCallbacks = taggedServerListeners .map((l) => l.serverListener.drainServer) .filter(isDefined); const drainServers = drainServerCallbacks.length ? async () => { await Promise.all( drainServerCallbacks.map((drainServer) => drainServer()), ); } : null; // Find the renderLandingPage callback, if one is provided. If the user // installed ApolloServerPluginLandingPageDisabled then there may be none // found. On the other hand, if the user installed a landingPage plugin, // then both the implicit installation of // ApolloServerPluginLandingPage*Default and the other plugin will be // found; we skip the implicit plugin. let taggedServerListenersWithRenderLandingPage = taggedServerListeners.filter((l) => l.serverListener.renderLandingPage); if (taggedServerListenersWithRenderLandingPage.length > 1) { taggedServerListenersWithRenderLandingPage = taggedServerListenersWithRenderLandingPage.filter( (l) => !l.installedImplicitly, ); } let landingPage: LandingPage | null = null; if (taggedServerListenersWithRenderLandingPage.length > 1) { throw Error('Only one plugin can implement renderLandingPage.'); } else if (taggedServerListenersWithRenderLandingPage.length) { landingPage = await taggedServerListenersWithRenderLandingPage[0].serverListener .renderLandingPage!(); } const toDisposeLast = this.maybeRegisterTerminationSignalHandlers( ['SIGINT', 'SIGTERM'], startedInBackground, ); this.internals.state = { phase: 'started', schemaManager, drainServers, landingPage, toDispose, toDisposeLast, }; } catch (maybeError: unknown) { const error = ensureError(maybeError); try { await Promise.all( this.internals.plugins.map(async (plugin) => plugin.startupDidFail?.({ error }), ), ); } catch (pluginError) { this.logger.error(`startupDidFail hook threw: ${pluginError}`); } this.internals.state = { phase: 'failed to start', error, }; throw error; } finally { barrier.resolve(); } } private maybeRegisterTerminationSignalHandlers( signals: NodeJS.Signals[], startedInBackground: boolean, ): (() => Promise<void>)[] { const toDisposeLast: (() => Promise<void>)[] = []; // We handle signals if it was explicitly requested // (stopOnTerminationSignals === true), or if we're in Node, not in a test, // not in a serverless framework (which we guess based on whether they // called // startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests), // and it wasn't explicitly turned off. (We only actually register the // signal handlers once we've successfully started up, because there's // nothing to stop otherwise.) if ( this.internals.stopOnTerminationSignals === false || (this.internals.stopOnTerminationSignals === undefined && !( isNodeLike && this.internals.nodeEnv !== 'test' && !startedInBackground )) ) { return toDisposeLast; } let receivedSignal = false; const signalHandler: NodeJS.SignalsListener = async (signal) => { if (receivedSignal) { // If we receive another SIGINT or SIGTERM while we're waiting // for the server to stop, just ignore it. return; } receivedSignal = true; try { await this.stop(); } catch (e) { this.logger.error(`stop() threw during ${signal} shutdown`); this.logger.error(e); // Can't rely on the signal handlers being removed. process.exit(1); } // Note: this.stop will call the toDisposeLast handlers below, so at // this point this handler will have been removed and we can re-kill // ourself to die with the appropriate signal exit status. this.stop // takes care to call toDisposeLast last, so the signal handler isn't // removed until after the rest of shutdown happens. process.kill(process.pid, signal); }; signals.forEach((signal) => { process.on(signal, signalHandler); toDisposeLast.push(async () => { process.removeListener(signal, signalHandler); }); }); return toDisposeLast; } // This method is called at the beginning of each GraphQL request by // `executeHTTPGraphQLRequest` and `executeOperation`. Most of its logic is // only helpful if you started the server in the background (ie, for // serverless frameworks): unless you're in a serverless framework, you should // have called `await server.start()` before the server got to the point of // running GraphQL requests (`assertStarted` calls in the framework // integrations verify that) and so the only cases for non-serverless // frameworks that this should hit are 'started', 'stopping', and 'stopped'. // But if you started the server in the background (with // startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests), this // lets the server wait until fully started before serving operations. private async _ensureStarted(): Promise<RunningServerState> { while (true) { switch (this.internals.state.phase) { case 'initialized': // This error probably won't happen: serverless framework integrations // should call // `startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests` // for you, and other frameworks call `assertStarted` before setting // things up enough to make calling this function possible. throw new Error( 'You need to call `server.start()` before using your Apollo Server.', ); case 'starting': await this.internals.state.barrier; // continue the while loop break; case 'failed to start': // First we log the error that prevented startup (which means it will // get logged once for every GraphQL operation). this.logStartupError(this.internals.state.error); // Now make the operation itself fail. // We intentionally do not re-throw actual startup error as it may contain // implementation details and this error will propagate to the client. throw new Error( 'This data graph is missing a valid configuration. More details may be available in the server logs.', ); case 'started': case 'draining': // We continue to run operations while draining. return this.internals.state; case 'stopping': case 'stopped': this.logger.warn( 'A GraphQL operation was received during server shutdown. The ' + 'operation will fail. Consider draining the HTTP server on shutdown; ' + 'see https://go.apollo.dev/s/drain for details.', ); throw new Error( `Cannot execute GraphQL operations ${ this.internals.state.phase === 'stopping' ? 'while the server is stopping' : 'after the server has stopped' }.'`, ); default: throw new UnreachableCaseError(this.internals.state); } } } // Framework integrations should call this to ensure that you've properly // started your server before you get anywhere close to actually listening for // incoming requests. // // There's a special case that if you called // `startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests` and // it hasn't finished starting up yet, this works too. This is intended for // cases like a serverless integration (say, Google Cloud Functions) that // calls // `startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests` for // you and then immediately sets up an integration based on another middleware // like `expressMiddleware` which calls this function. We'd like this to be // OK, but we still want normal Express users to start their ApolloServer // before setting up their HTTP server unless they know what they are doing // well enough to call the function with the long name themselves. public assertStarted(expressionForError: string) { if ( this.internals.state.phase !== 'started' && this.internals.state.phase !== 'draining' && !( this.internals.state.phase === 'starting' && this.internals.state.startedInBackground ) ) { throw new Error( 'You must `await server.start()` before calling `' + expressionForError + '`', ); } } // Given an error that occurred during Apollo Server startup, log it with a // helpful message. This should happen when you call // `startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests` (ie, // in serverless frameworks); with other frameworks, you must `await // server.start()` which will throw the startup error directly instead of // logging. This gets called both immediately when the startup error happens, // and on all subsequent requests. private logStartupError(err: Error) { this.logger.error( 'An error occurred during Apollo Server startup. All GraphQL requests ' + 'will now fail. The startup error was: ' + (err?.message || err), ); } private static constructSchema<TContext extends BaseContext>( config: ApolloServerOptionsWithStaticSchema<TContext>, ): GraphQLSchema { if (config.schema) { return config.schema; } const { typeDefs, resolvers } = config; const augmentedTypeDefs = Array.isArray(typeDefs) ? typeDefs : [typeDefs]; // For convenience, we allow you to pass a few options that we pass through // to a particular version of `@graphql-tools/schema`'s // `makeExecutableSchema`. If you want to use more of this function's // features or have more control over the version of the packages used, just // call it yourself like `new ApolloServer({schema: // makeExecutableSchema(...)})`. return makeExecutableSchema({ typeDefs: augmentedTypeDefs, resolvers, }); } private static generateSchemaDerivedData( schema: GraphQLSchema, // null means don't use a documentStore at all. // missing/undefined means use the default (creating a new one each // time). // defined means wrap this one in a random prefix for each new schema. providedDocumentStore: DocumentStore | null | undefined, ): SchemaDerivedData { // Instead of waiting for the first operation execution against the schema // to find out if it's a valid schema or not, check right now. In the // non-gateway case, if this throws then the `new ApolloServer` call will // throw. In the gateway case if this throws then it will log a message and // just not update the schema (although oddly the message will claim that // the schema is updating). assertValidSchema(schema); return { schema, // The DocumentStore is schema-derived because we put documents in it // after checking that they pass GraphQL validation against the schema and // use this to skip validation as well as parsing. So we can't reuse the // same DocumentStore for different schemas because that might make us // treat invalid operations as valid. If we're using the default // DocumentStore, then we just create it from scratch each time we get a // new schema. If we're using a user-provided DocumentStore, then we use // the schema hash as a prefix. documentStore: providedDocumentStore === undefined ? new InMemoryLRUCache<DocumentNode>() : providedDocumentStore, documentStoreKeyPrefix: providedDocumentStore ? `${computeCoreSchemaHash(printSchema(schema))}:` : '', }; } public async stop() { switch (this.internals.state.phase) { case 'initialized': case 'starting': case 'failed to start': throw Error( 'apolloServer.stop() should only be called after `await apolloServer.start()` has succeeded', ); // Calling stop more than once should have the same result as the first time. case 'stopped': if (this.internals.state.stopError) { throw this.internals.state.stopError; } return; // Two parallel calls to stop; just wait for the other one to finish and // do whatever it did. case 'stopping': case 'draining': { await this.internals.state.barrier; // The cast here is because TS doesn't understand that this.state can // change during the await // (https://github.com/microsoft/TypeScript/issues/9998). const state = this.internals.state as ServerState; if (state.phase !== 'stopped') { throw Error(`Surprising post-stopping state ${state.phase}`); } if (state.stopError) { throw state.stopError; } return; } case 'started': // This is handled by the rest of the function. break; default: throw new UnreachableCaseError(this.internals.state); } const barrier = resolvable(); const { schemaManager, drainServers, landingPage, toDispose, toDisposeLast, } = this.internals.state; // Commit to stopping and start draining servers. this.internals.state = { phase: 'draining', barrier, schemaManager, landingPage, }; try { await drainServers?.(); // Servers are drained. Prevent further operations from starting and call // stop handlers. this.internals.state = { phase: 'stopping', barrier }; // We run shutdown handlers in two phases because we don't want to turn // off our signal listeners (ie, allow signals to kill the process) until // we've done the important parts of shutdown like running serverWillStop // handlers. (We can make this more generic later if it's helpful.) await Promise.all([...toDispose].map((dispose) => dispose())); await Promise.all([...toDisposeLast].map((dispose) => dispose())); } catch (stopError) { this.internals.state = { phase: 'stopped', stopError: stopError as Error, }; barrier.resolve(); throw stopError; } this.internals.state = { phase: 'stopped', stopError: null }; } private async addDefaultPlugins() { const { plugins, apolloConfig, nodeEnv, hideSchemaDetailsFromClientErrors, } = this.internals; const isDev = nodeEnv !== 'production'; const alreadyHavePluginWithInternalId = (id: InternalPluginId) => plugins.some( (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')) { const { ApolloServerPluginCacheControl } = await import( './plugin/cacheControl/index.js' ); plugins.push(ApolloServerPluginCacheControl()); } } // Special case: usage reporting is on by default (and first!) if you // configure an API key. { const alreadyHavePlugin = alreadyHavePluginWithInternalId('UsageReporting'); if (!alreadyHavePlugin && apolloConfig.key) { if (apolloConfig.graphRef) { // Keep this plugin first so it wraps everything. (Unfortunately despite // the fact that the person who wrote this line also was the original // author of the comment above in #1105, they don't quite understand why this was important.) const { ApolloServerPluginUsageReporting } = await import( './plugin/usageReporting/index.js' ); plugins.unshift( ApolloServerPluginUsageReporting({ __onlyIfSchemaIsNotSubgraph: true, }), ); } else { this.logger.warn( 'You have specified an Apollo key but have not specified a graph ref; usage ' + 'reporting is disabled. To enable usage reporting, set the `APOLLO_GRAPH_REF` ' + 'environment variable to `your-graph-id@your-graph-variant`. To disable this ' + 'warning, install `ApolloServerPluginUsageReportingDisabled`.', ); } } } // Special case: schema reporting can be turned on via environment variable. { const alreadyHavePlugin = alreadyHavePluginWithInternalId('SchemaReporting'); const enabledViaEnvVar = process.env.APOLLO_SCHEMA_REPORTING === 'true'; if (!alreadyHavePlugin && enabledViaEnvVar) { if (apolloConfig.key) { const { ApolloServerPluginSchemaReporting } = await import( './plugin/schemaReporting/index.js' ); plugins.push(ApolloServerPluginSchemaReporting()); } else { throw new Error( "You've enabled schema reporting by setting the APOLLO_SCHEMA_REPORTING " + 'environment variable to true, but you also need to provide your ' + 'Apollo API key, via the APOLLO_KEY environment ' + 'variable or via `new ApolloServer({apollo: {key})', ); } } } // Special case: inline tracing is on by default for federated schemas. { const alreadyHavePlugin = alreadyHavePluginWithInternalId('InlineTrace'); if (!alreadyHavePlugin) { // If we haven't explicitly disabled inline tracing via // ApolloServerPluginInlineTraceDisabled or explicitly installed our own // ApolloServerPluginInlineTrace, we set up inline tracing in "only if // federated" mode. (This is slightly different than the // pre-ApolloServerPluginInlineTrace where we would also avoid doing // this if an API key was configured and log a warning.) const { ApolloServerPluginInlineTrace } = await import( './plugin/inlineTrace/index.js' ); plugins.push( ApolloServerPluginInlineTrace({ __onlyIfSchemaIsSubgraph: true }), ); } } // Special case: If we're not in production, show our default landing page. // // This works a bit differently from the other implicitly installed plugins, // which rely entirely on the __internal_plugin_id__ to decide whether the // plugin takes effect. That's because we want third-party plugins to be // able to provide a landing page that overrides the default landing page, // without them having to know about __internal_plugin_id__. So unless we // actively disable the default landing page with // ApolloServerPluginLandingPageDisabled, we install the default landing // page, but with a special flag that _start() uses to ignore it if some // other plugin defines a renderLandingPage callback. (We can't just look // now to see if the plugin defines renderLandingPage because we haven't run // serverWillStart yet.) const alreadyHavePlugin = alreadyHavePluginWithInternalId( 'LandingPageDisabled', ); if (!alreadyHavePlugin) { const { ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageProductionDefault, } = await import('./plugin/landingPage/default/index.js'); const plugin: ApolloServerPlugin<TContext> = isDev ? ApolloServerPluginLandingPageLocalDefault() : ApolloServerPluginLandingPageProductionDefault(); if (!isImplicitlyInstallablePlugin(plugin)) { throw Error( 'default landing page plugin should be implicitly installable?', ); } plugin.__internal_installed_implicitly__ = true; plugins.push(plugin); } { const alreadyHavePlugin = alreadyHavePluginWithInternalId('DisableSuggestions'); if (hideSchemaDetailsFromClientErrors && !alreadyHavePlugin) { const { ApolloServerPluginDisableSuggestions } = await import( './plugin/disableSuggestions/index.js' ); plugins.push(ApolloServerPluginDisableSuggestions()); } } } public addPlugin(plugin: ApolloServerPlugin<TContext>) { if (this.internals.state.phase !== 'initialized') { throw new Error("Can't add plugins after the server has started"); } this.internals.plugins.push(plugin); } public async executeHTTPGraphQLRequest({ httpGraphQLRequest, context, }: { httpGraphQLRequest: HTTPGraphQLRequest; context: ContextThunk<TContext>; }): Promise<HTTPGraphQLResponse> { try { let runningServerState; try { runningServerState = await this._ensureStarted(); } catch (error: unknown) { // This is typically either the masked error from when background startup // failed, or related to invoking this function before startup or // during/after shutdown (due to lack of draining). return await this.errorResponse(error, httpGraphQLRequest); } if ( runningServerState.landingPage && this.prefersHTML(httpGraphQLRequest) ) { let renderedHtml; if (typeof runningServerState.landingPage.html === 'string') { renderedHtml = runningServerState.landingPage.html; } else { try { renderedHtml = await runningServerState.landingPage.html(); } catch (maybeError: unknown) { const error = ensureError(maybeError); this.logger.error(`Landing page \`html\` function threw: ${error}`); return await this.errorResponse(error, httpGraphQLRequest); } } return { headers: new HeaderMap([['content-type', 'text/html']]), body: { kind: 'complete', string: renderedHtml, }, }; } // If enabled, check to ensure that this request was preflighted before doing // anything real (such as running the context function). if (this.internals.csrfPreventionRequestHeaders) { preventCsrf( httpGraphQLRequest.headers, this.internals.csrfPreventionRequestHeaders, ); } let contextValue: TContext; try { contextValue = await context(); } catch (maybeError: unknown) { const error = ensureError(maybeError); try { await Promise.all( this.internals.plugins.map(async (plugin) => plugin.contextCreationDidFail?.({ error, }), ), ); } catch (pluginError) { this.logger.error( `contextCreationDidFail hook threw: ${pluginError}`, ); } // If some random function threw, add a helpful prefix when converting // to GraphQLError. If it was already a GraphQLError, trust that the // message was chosen thoughtfully and leave off the prefix. return await this.errorResponse( ensureGraphQLError(error, 'Context creation failed: '), httpGraphQLRequest, ); } return await runPotentiallyBatchedHttpQuery( this, httpGraphQLRequest, contextValue, runningServerState.schemaManager.getSchemaDerivedData(), this.internals, ); } catch (maybeError_: unknown) { const maybeError = maybeError_; // fixes inference because catch vars are not const if ( maybeError instanceof GraphQLError && maybeError.extensions.code === ApolloServerErrorCode.BAD_REQUEST ) { try { await Promise.all( this.internals.plugins.map(async (plugin) => plugin.invalidRequestWasReceived?.({ error: maybeError }), ), ); } catch (pluginError) { this.logger.error( `invalidRequestWasReceived hook threw: ${pluginError}`, ); } } return await this.errorResponse(maybeError, httpGraphQLRequest); } } private async errorResponse( error: unknown, requestHead: HTTPGraphQLHead, ): Promise<HTTPGraphQLResponse> { const { formattedErrors, httpFromErrors } = normalizeAndFormatErrors( [error], { includeStacktraceInErrorResponses: this.internals.includeStacktraceInErrorResponses, formatError: this.internals.formatError, }, ); return { status: httpFromErrors.status ?? 500, headers: new HeaderMap([ ...httpFromErrors.headers, [ 'content-type', // Note that we may change the default to // 'application/graphql-response+json' by 2025 as encouraged by the // graphql-over-http spec. It's maybe a bit bad for us to provide // an application/json response if they send `accept: foo/bar`, // but we're already providing some sort of bad request error, and // it's probably more useful for them to fix the other error before // they deal with the `accept` header. chooseContentTypeForSingleResultResponse(requestHead) ?? MEDIA_TYPES.APPLICATION_JSON, ], ]), body: { kind: 'complete', string: await this.internals.stringifyResult({ errors: formattedErrors, }), }, }; } private prefersHTML(request: HTTPGraphQLRequest): boolean { const acceptHeader = request.headers.get('accept'); return ( request.method === 'GET' && !!acceptHeader && new Negotiator({ headers: { accept: acceptHeader }, }).mediaType([ // We need it to actively prefer text/html over less browser-y types; // eg, `accept: */*' should still go for JSON. Negotiator does tiebreak // by the order in the list we provide, so we put text/html last. MEDIA_TYPES.APPLICATION_JSON, MEDIA_TYPES.APPLICATION_GRAPHQL_RESPONSE_JSON, MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL, MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC, MEDIA_TYPES.TEXT_HTML, ]) === MEDIA_TYPES.TEXT_HTML ); } /** * This method is primarily meant for testing: it allows you to execute a * GraphQL operation via the request pipeline without going through the HTTP * layer. Note that this means that any handling you do in your server at the * HTTP level will not affect this call! * * For convenience, you can provide `request.query` either as a string or a * DocumentNode, in case you choose to use the gql tag in your tests. This is * just a convenience, not an optimization (we convert provided ASTs back into * string). * * The second object is an optional options object which includes the optional * `contextValue` object available in resolvers. * * You may specify the TData and TVariables generic types when calling this * method; Apollo Server does not validate that the returned data actually * matches the structure of TData. (Typically these types are created by a * code generation tool.) Note that this does not enforce that `variables` is * provided at all, just that it has the right type if provided. */ public async executeOperation< TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues, >( this: ApolloServer<BaseContext>, request: Omit<GraphQLRequest<TVariables>, 'query'> & { query?: string | DocumentNode | TypedQueryDocumentNode<TData, TVariables>; }, ): Promise<GraphQLResponse<TData>>; public async executeOperation< TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues, >( request: Omit<GraphQLRequest<TVariables>, 'query'> & { query?: string | DocumentNode | TypedQueryDocumentNode<TData, TVariables>; }, options?: ExecuteOperationOptions<TContext>, ): Promise<GraphQLResponse<TData>>; async executeOperation< TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues, >( request: Omit<GraphQLRequest<TVariables>, 'query'> & { // We should consider supporting TypedDocumentNode from // `@graphql-typed-document-node/core` as well, as it is more popular than // the newer built-in type. query?: string | DocumentNode | TypedQueryDocumentNode<TData, TVariables>; }, options: ExecuteOperationOptions<TContext> = {}, ): Promise<GraphQLResponse<TData>> { // Since this function is mostly for testing, you don't need to explicitly // start your server before calling it. (That also means you can use it with // `apollo-server` which doesn't support `start()`.) if (this.internals.state.phase === 'initialized') { await this.start(); } const schemaDerivedData = ( await this._ensureStarted() ).schemaManager.getSchemaDerivedData(); // For convenience, this function lets you pass either a string or an AST, // but we normalize to string. const graphQLRequest: GraphQLRequest = { ...request, query: request.query && typeof request.query !== 'string' ? print(request.query) : request.query, }; const response: GraphQLResponse = await internalExecuteOperation( { server: this, graphQLRequest, internals: this.internals, schemaDerivedData, sharedResponseHTTPGraphQLHead: null, }, options, ); // It's your job to set a