UNPKG

50 kBPlain TextView Raw
1import {
2 makeExecutableSchema,
3 addMockFunctionsToSchema,
4 GraphQLParseOptions,
5} from 'graphql-tools';
6import { Server as NetServer } from 'net';
7import { Server as TlsServer } from 'tls';
8import { Server as HttpServer } from 'http';
9import { Http2Server, Http2SecureServer } from 'http2';
10import { Server as HttpsServer } from 'https';
11import loglevel from 'loglevel';
12import {
13 execute,
14 GraphQLSchema,
15 subscribe,
16 ExecutionResult,
17 GraphQLError,
18 GraphQLFieldResolver,
19 ValidationContext,
20 FieldDefinitionNode,
21 DocumentNode,
22} from 'graphql';
23import resolvable, { Resolvable } from '@josephg/resolvable';
24import { GraphQLExtension } from 'graphql-extensions';
25import {
26 InMemoryLRUCache,
27 PrefixingKeyValueCache,
28} from 'apollo-server-caching';
29import {
30 ApolloServerPlugin,
31 GraphQLServiceContext,
32 GraphQLServerListener,
33} from 'apollo-server-plugin-base';
34import runtimeSupportsUploads from './utils/runtimeSupportsUploads';
35
36import {
37 SubscriptionServer,
38 ExecutionParams,
39} from 'subscriptions-transport-ws';
40
41import WebSocket from 'ws';
42
43import { formatApolloErrors } from 'apollo-server-errors';
44import { GraphQLServerOptions, PersistedQueryOptions } from './graphqlOptions';
45
46import {
47 Config,
48 Context,
49 ContextFunction,
50 SubscriptionServerOptions,
51 FileUploadOptions,
52 PluginDefinition,
53 GraphQLService,
54} from './types';
55
56import { gql } from './index';
57
58import {
59 createPlaygroundOptions,
60 PlaygroundRenderPageOptions,
61} from './playground';
62
63import { generateSchemaHash } from './utils/schemaHash';
64import { isDirectiveDefined } from './utils/isDirectiveDefined';
65import {
66 processGraphQLRequest,
67 GraphQLRequestContext,
68 GraphQLRequest,
69 APQ_CACHE_PREFIX,
70} from './requestPipeline';
71
72import { Headers } from 'apollo-server-env';
73import { buildServiceDefinition } from '@apollographql/apollo-tools';
74import { plugin as pluginTracing } from 'apollo-tracing';
75import { Logger, SchemaHash, ApolloConfig } from 'apollo-server-types';
76import {
77 plugin as pluginCacheControl,
78 CacheControlExtensionOptions,
79} from 'apollo-cache-control';
80import { cloneObject } from './runHttpQuery';
81import isNodeLike from './utils/isNodeLike';
82import { determineApolloConfig } from './determineApolloConfig';
83import {
84 ApolloServerPluginSchemaReporting,
85 ApolloServerPluginUsageReportingFromLegacyOptions,
86 ApolloServerPluginSchemaReportingOptions,
87 ApolloServerPluginInlineTrace,
88 ApolloServerPluginInlineTraceOptions,
89 ApolloServerPluginUsageReporting,
90} from './plugin';
91import { InternalPluginId, pluginIsInternal } from './plugin/internalPlugin';
92
93const NoIntrospection = (context: ValidationContext) => ({
94 Field(node: FieldDefinitionNode) {
95 if (node.name.value === '__schema' || node.name.value === '__type') {
96 context.reportError(
97 new GraphQLError(
98 'GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production',
99 [node],
100 ),
101 );
102 }
103 },
104});
105
106const forbidUploadsForTesting =
107 process && process.env.NODE_ENV === 'test' && !runtimeSupportsUploads;
108
109function approximateObjectSize<T>(obj: T): number {
110 return Buffer.byteLength(JSON.stringify(obj), 'utf8');
111}
112
113type SchemaDerivedData = {
114 schema: GraphQLSchema;
115 schemaHash: SchemaHash;
116 extensions: Array<() => GraphQLExtension>;
117 // A store that, when enabled (default), will store the parsed and validated
118 // versions of operations in-memory, allowing subsequent parses/validates
119 // on the same operation to be executed immediately.
120 documentStore?: InMemoryLRUCache<DocumentNode>;
121};
122
123type ServerState =
124 | { phase: 'initialized with schema'; schemaDerivedData: SchemaDerivedData }
125 | { phase: 'initialized with gateway'; gateway: GraphQLService }
126 | { phase: 'starting'; barrier: Resolvable<void> }
127 | {
128 phase: 'invoking serverWillStart';
129 barrier: Resolvable<void>;
130 schemaDerivedData: SchemaDerivedData;
131 }
132 | { phase: 'failed to start'; error: Error; loadedSchema: boolean }
133 | {
134 phase: 'started';
135 schemaDerivedData: SchemaDerivedData;
136 }
137 | { phase: 'stopping'; barrier: Resolvable<void> }
138 | { phase: 'stopped'; stopError: Error | null };
139
140// Throw this in places that should be unreachable (because all other cases have
141// been handled, reducing the type of the argument to `never`). TypeScript will
142// complain if in fact there is a valid type for the argument.
143class UnreachableCaseError extends Error {
144 constructor(val: never) {
145 super(`Unreachable case: ${val}`);
146 }
147}
148export class ApolloServerBase {
149 private logger: Logger;
150 public subscriptionsPath?: string;
151 public graphqlPath: string = '/graphql';
152 public requestOptions: Partial<GraphQLServerOptions<any>> = Object.create(
153 null,
154 );
155
156 private context?: Context | ContextFunction;
157 private apolloConfig: ApolloConfig;
158 protected plugins: ApolloServerPlugin[] = [];
159
160 protected subscriptionServerOptions?: SubscriptionServerOptions;
161 protected uploadsConfig?: FileUploadOptions;
162
163 // set by installSubscriptionHandlers.
164 private subscriptionServer?: SubscriptionServer;
165
166 // the default version is specified in playground.ts
167 protected playgroundOptions?: PlaygroundRenderPageOptions;
168
169 private parseOptions: GraphQLParseOptions;
170 private config: Config;
171 private state: ServerState;
172 /** @deprecated: This is undefined for servers operating as gateways, and will be removed in a future release **/
173 protected schema?: GraphQLSchema;
174 private toDispose = new Set<() => Promise<void>>();
175 private toDisposeLast = new Set<() => Promise<void>>();
176 private experimental_approximateDocumentStoreMiB: Config['experimental_approximateDocumentStoreMiB'];
177
178 // The constructor should be universal across all environments. All environment specific behavior should be set by adding or overriding methods
179 constructor(config: Config) {
180 if (!config) throw new Error('ApolloServer requires options.');
181 this.config = config;
182 const {
183 context,
184 resolvers,
185 schema,
186 schemaDirectives,
187 modules,
188 typeDefs,
189 parseOptions = {},
190 introspection,
191 mocks,
192 mockEntireSchema,
193 extensions,
194 subscriptions,
195 uploads,
196 playground,
197 plugins,
198 gateway,
199 cacheControl,
200 experimental_approximateDocumentStoreMiB,
201 stopOnTerminationSignals,
202 apollo,
203 engine,
204 ...requestOptions
205 } = config;
206
207 if (engine !== undefined && apollo) {
208 throw new Error(
209 'You cannot provide both `engine` and `apollo` to `new ApolloServer()`. ' +
210 'For details on how to migrate all of your options out of `engine`, see ' +
211 'https://go.apollo.dev/s/migration-engine-plugins',
212 );
213 }
214
215 // Setup logging facilities
216 if (config.logger) {
217 this.logger = config.logger;
218 } else {
219 // If the user didn't provide their own logger, we'll initialize one.
220 const loglevelLogger = loglevel.getLogger('apollo-server');
221
222 // We don't do much logging in Apollo Server right now. There's a notion
223 // of a `debug` flag, which changes stack traces in some error messages,
224 // and adds a bit of debug logging to some plugins. `info` is primarily
225 // used for startup logging in plugins. We'll default to `info` so you
226 // get to see that startup logging.
227 if (this.config.debug === true) {
228 loglevelLogger.setLevel(loglevel.levels.DEBUG);
229 } else {
230 loglevelLogger.setLevel(loglevel.levels.INFO);
231 }
232
233 this.logger = loglevelLogger;
234 }
235
236 this.apolloConfig = determineApolloConfig(apollo, engine, this.logger);
237
238 if (gateway && (modules || schema || typeDefs || resolvers)) {
239 throw new Error(
240 'Cannot define both `gateway` and any of: `modules`, `schema`, `typeDefs`, or `resolvers`',
241 );
242 }
243
244 this.parseOptions = parseOptions;
245 this.context = context;
246
247 // While reading process.env is slow, a server should only be constructed
248 // once per run, so we place the env check inside the constructor. If env
249 // should be used outside of the constructor context, place it as a private
250 // or protected field of the class instead of a global. Keeping the read in
251 // the constructor enables testing of different environments
252 const isDev = process.env.NODE_ENV !== 'production';
253
254 // if this is local dev, introspection should turned on
255 // in production, we can manually turn introspection on by passing {
256 // introspection: true } to the constructor of ApolloServer
257 if (
258 (typeof introspection === 'boolean' && !introspection) ||
259 (introspection === undefined && !isDev)
260 ) {
261 const noIntro = [NoIntrospection];
262 requestOptions.validationRules = requestOptions.validationRules
263 ? requestOptions.validationRules.concat(noIntro)
264 : noIntro;
265 }
266
267 if (!requestOptions.cache) {
268 requestOptions.cache = new InMemoryLRUCache();
269 }
270
271 if (requestOptions.persistedQueries !== false) {
272 const { cache: apqCache = requestOptions.cache!, ...apqOtherOptions } =
273 requestOptions.persistedQueries || Object.create(null);
274
275 requestOptions.persistedQueries = {
276 cache: new PrefixingKeyValueCache(apqCache, APQ_CACHE_PREFIX),
277 ...apqOtherOptions,
278 };
279 } else {
280 // the user does not want to use persisted queries, so we remove the field
281 delete requestOptions.persistedQueries;
282 }
283
284 this.requestOptions = requestOptions as GraphQLServerOptions;
285
286 if (uploads !== false && !forbidUploadsForTesting) {
287 if (this.supportsUploads()) {
288 if (!runtimeSupportsUploads) {
289 printNodeFileUploadsMessage(this.logger);
290 throw new Error(
291 '`graphql-upload` is no longer supported on Node.js < v8.5.0. ' +
292 'See https://bit.ly/gql-upload-node-6.',
293 );
294 }
295
296 if (uploads === true || typeof uploads === 'undefined') {
297 this.uploadsConfig = {};
298 } else {
299 this.uploadsConfig = uploads;
300 }
301 //This is here to check if uploads is requested without support. By
302 //default we enable them if supported by the integration
303 } else if (uploads) {
304 throw new Error(
305 'This implementation of ApolloServer does not support file uploads because the environment cannot accept multi-part forms',
306 );
307 }
308 }
309
310 if (gateway && subscriptions !== false) {
311 // TODO: this could be handled by adjusting the typings to keep gateway configs and non-gateway configs separate.
312 throw new Error(
313 [
314 'Subscriptions are not yet compatible with the gateway.',
315 "Set `subscriptions: false` in Apollo Server's constructor to",
316 'explicitly disable subscriptions (which are on by default)',
317 'and allow for gateway functionality.',
318 ].join(' '),
319 );
320 } else if (subscriptions !== false) {
321 if (this.supportsSubscriptions()) {
322 if (subscriptions === true || typeof subscriptions === 'undefined') {
323 this.subscriptionServerOptions = {
324 path: this.graphqlPath,
325 };
326 } else if (typeof subscriptions === 'string') {
327 this.subscriptionServerOptions = { path: subscriptions };
328 } else {
329 this.subscriptionServerOptions = {
330 path: this.graphqlPath,
331 ...subscriptions,
332 };
333 }
334 // This is part of the public API.
335 this.subscriptionsPath = this.subscriptionServerOptions.path;
336
337 //This is here to check if subscriptions are requested without support. By
338 //default we enable them if supported by the integration
339 } else if (subscriptions) {
340 throw new Error(
341 'This implementation of ApolloServer does not support GraphQL subscriptions.',
342 );
343 }
344 }
345
346 this.playgroundOptions = createPlaygroundOptions(playground);
347
348 // Plugins will be instantiated if they aren't already, and this.plugins
349 // is populated accordingly.
350 this.ensurePluginInstantiation(plugins);
351
352 // We handle signals if it was explicitly requested, or if we're in Node,
353 // not in a test, and it wasn't explicitly turned off. (For backwards
354 // compatibility, we check both 'stopOnTerminationSignals' and
355 // 'engine.handleSignals'.)
356 if (
357 typeof stopOnTerminationSignals === 'boolean'
358 ? stopOnTerminationSignals
359 : typeof engine === 'object' &&
360 typeof engine.handleSignals === 'boolean'
361 ? engine.handleSignals
362 : isNodeLike && process.env.NODE_ENV !== 'test'
363 ) {
364 const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'];
365 let receivedSignal = false;
366 signals.forEach((signal) => {
367 // Note: Node only started sending signal names to signal events with
368 // Node v10 so we can't use that feature here.
369 const handler: NodeJS.SignalsListener = async () => {
370 if (receivedSignal) {
371 // If we receive another SIGINT or SIGTERM while we're waiting
372 // for the server to stop, just ignore it.
373 return;
374 }
375 receivedSignal = true;
376 try {
377 await this.stop();
378 } catch (e) {
379 this.logger.error(`stop() threw during ${signal} shutdown`);
380 this.logger.error(e);
381 // Can't rely on the signal handlers being removed.
382 process.exit(1);
383 }
384 // Note: this.stop will call the toDisposeLast handlers below, so at
385 // this point this handler will have been removed and we can re-kill
386 // ourself to die with the appropriate signal exit status. this.stop
387 // takes care to call toDisposeLast last, so the signal handler isn't
388 // removed until after the rest of shutdown happens.
389 process.kill(process.pid, signal);
390 };
391 process.on(signal, handler);
392 this.toDisposeLast.add(async () => {
393 process.removeListener(signal, handler);
394 });
395 });
396 }
397
398 if (gateway) {
399 // ApolloServer has been initialized but we have not yet tried to load the
400 // schema from the gateway. That will wait until the user calls
401 // `server.start()`, or until `ensureStarting` or `ensureStarted` are
402 // called. (In the case of a serverless framework integration,
403 // `ensureStarting` is automatically called at the end of the
404 // constructor.)
405 this.state = { phase: 'initialized with gateway', gateway };
406
407 // The main thing that the Gateway does is replace execution with
408 // its own executor. It would be awkward if you always had to pass
409 // `gateway: gateway, executor: gateway` to this constructor, so
410 // we let specifying `gateway` be a shorthand for the above.
411 // (We won't actually invoke the executor until after we're successfully
412 // called `gateway.load`.)
413 this.requestOptions.executor = gateway.executor;
414 } else {
415 // We construct the schema synchronously so that we can fail fast if
416 // the schema can't be constructed, and so that installSubscriptionHandlers
417 // and the deprecated `.schema` field (neither of which have ever worked
418 // with the gateway) are available immediately after the constructor returns.
419 this.state = {
420 phase: 'initialized with schema',
421 schemaDerivedData: this.generateSchemaDerivedData(
422 this.constructSchema(),
423 ),
424 };
425 // This field is deprecated; users who are interested in learning
426 // their server's schema should instead make a plugin with serverWillStart,
427 // or register onSchemaChange on their gateway. It is only ever
428 // set for non-gateway servers.
429 this.schema = this.state.schemaDerivedData.schema;
430 }
431
432 // The main entry point (createHandler) to serverless frameworks generally
433 // needs to be called synchronously from the top level of your entry point,
434 // unlike (eg) applyMiddleware, so we can't expect you to `await
435 // server.start()` before calling it. So we kick off the start
436 // asynchronously from the constructor, and failures are logged and cause
437 // later requests to fail (in ensureStarted, called by
438 // graphQLServerOptions). There's no way to make "the whole server fail"
439 // separately from making individual requests fail, but that's not entirely
440 // unreasonable for a "serverless" model.
441 if (this.serverlessFramework()) {
442 this.ensureStarting();
443 }
444 }
445
446 // used by integrations to synchronize the path with subscriptions, some
447 // integrations do not have paths, such as lambda
448 public setGraphQLPath(path: string) {
449 this.graphqlPath = path;
450 }
451
452 // Awaiting a call to `start` ensures that a schema has been loaded and that
453 // all plugin `serverWillStart` hooks have been called. If either of these
454 // processes throw, `start` will (async) throw as well.
455 //
456 // If you're using the batteries-included `apollo-server` package, you don't
457 // need to call `start` yourself (in fact, it will throw if you do so); its
458 // `listen` method takes care of that for you (this is why the actual logic is
459 // in the `_start` helper).
460 //
461 // If instead you're using an integration package, you are highly encouraged
462 // to await a call to `start` immediately after creating your `ApolloServer`,
463 // before attaching it to your web framework and starting to accept requests.
464 // `start` should only be called once; if it throws and you'd like to retry,
465 // just create another `ApolloServer`. (Note that this paragraph does not
466 // apply to "serverless framework" integrations like Lambda.)
467 //
468 // For backwards compatibility with the pre-2.22 API, you are not required to
469 // call start() yourself (this may change in AS3). Most integration packages
470 // call the protected `ensureStarting` when you first interact with them,
471 // which kicks off a "background" call to `start` if you haven't called it
472 // yourself. Then `graphQLServerOptions` (which is called before processing)
473 // each incoming GraphQL request) calls `ensureStarted` which waits for
474 // `start` to successfully complete (possibly by calling it itself), and
475 // throws a redacted error if `start` was not successful. If `start` is
476 // invoked implicitly by either of these mechanisms, any error that it throws
477 // will be logged when they occur and then again on every subsequent
478 // `graphQLServerOptions` call (ie, every GraphQL request). Note that start
479 // failures are not recoverable without creating a new ApolloServer. You are
480 // highly encouraged to make these backwards-compatibility paths into no-ops
481 // by awaiting a call to `start` yourself.
482 //
483 // Serverless integrations like Lambda (which override `serverlessFramework()`
484 // to return true) do not support calling `start()`, because their lifecycle
485 // doesn't allow you to wait before assigning a handler or allowing the
486 // handler to be called. So they call `ensureStarting` at the end of the
487 // constructor, and don't really differentiate between startup failures and
488 // request failures. This is hopefully appropriate for a "serverless"
489 // framework. As above, startup failures result in returning a redacted error
490 // to the end user and logging the more detailed error.
491 public async start(): Promise<void> {
492 if (this.serverlessFramework()) {
493 throw new Error(
494 'When using an ApolloServer subclass from a serverless framework ' +
495 "package, you don't need to call start(); just call createHandler().",
496 );
497 }
498
499 return await this._start();
500 }
501
502 // This is protected so that it can be called from `apollo-server`. It is
503 // otherwise an internal implementation detail.
504 protected async _start(): Promise<void> {
505 const initialState = this.state;
506 if (
507 initialState.phase !== 'initialized with gateway' &&
508 initialState.phase !== 'initialized with schema'
509 ) {
510 throw new Error(
511 `called start() with surprising state ${initialState.phase}`,
512 );
513 }
514 const barrier = resolvable();
515 this.state = { phase: 'starting', barrier };
516 let loadedSchema = false;
517 try {
518 const schemaDerivedData =
519 initialState.phase === 'initialized with schema'
520 ? initialState.schemaDerivedData
521 : this.generateSchemaDerivedData(
522 await this.startGatewayAndLoadSchema(initialState.gateway),
523 );
524 loadedSchema = true;
525 this.state = {
526 phase: 'invoking serverWillStart',
527 barrier,
528 schemaDerivedData,
529 };
530
531 const service: GraphQLServiceContext = {
532 logger: this.logger,
533 schema: schemaDerivedData.schema,
534 schemaHash: schemaDerivedData.schemaHash,
535 apollo: this.apolloConfig,
536 serverlessFramework: this.serverlessFramework(),
537 engine: {
538 serviceID: this.apolloConfig.graphId,
539 apiKeyHash: this.apolloConfig.keyHash,
540 },
541 };
542
543 // The `persistedQueries` attribute on the GraphQLServiceContext was
544 // originally used by the operation registry, which shared the cache with
545 // it. This is no longer the case. However, while we are continuing to
546 // expand the support of the interface for `persistedQueries`, e.g. with
547 // additions like https://github.com/apollographql/apollo-server/pull/3623,
548 // we don't want to continually expand the API surface of what we expose
549 // to the plugin API. In this particular case, it certainly doesn't need
550 // to get the `ttl` default value which are intended for APQ only.
551 if (this.requestOptions.persistedQueries?.cache) {
552 service.persistedQueries = {
553 cache: this.requestOptions.persistedQueries.cache,
554 };
555 }
556
557 const serverListeners = (
558 await Promise.all(
559 this.plugins.map(
560 (plugin) =>
561 plugin.serverWillStart && plugin.serverWillStart(service),
562 ),
563 )
564 ).filter(
565 (maybeServerListener): maybeServerListener is GraphQLServerListener =>
566 typeof maybeServerListener === 'object' &&
567 !!maybeServerListener.serverWillStop,
568 );
569 this.toDispose.add(async () => {
570 await Promise.all(
571 serverListeners.map(({ serverWillStop }) => serverWillStop?.()),
572 );
573 });
574
575 this.state = { phase: 'started', schemaDerivedData };
576 } catch (error) {
577 this.state = { phase: 'failed to start', error, loadedSchema };
578 throw error;
579 } finally {
580 barrier.resolve();
581 }
582 }
583
584 /**
585 * @deprecated This deprecated method is provided for backwards compatibility
586 * with the pre-v2.22 API. It could be used for purposes similar to `start` or
587 * `ensureStarting`, and was used by integrations. It had odd error handling
588 * semantics, in that it would ignore any error that came from loading the
589 * schema, but would throw errors that came from `serverWillStart`. Anyone
590 * calling it should call `start` or `ensureStarting` instead.
591 */
592 protected async willStart() {
593 try {
594 this._start();
595 } catch (e) {
596 if (
597 this.state.phase === 'failed to start' &&
598 this.state.error === e &&
599 !this.state.loadedSchema
600 ) {
601 // For backwards compatibility with the odd semantics of the old
602 // willStart method, don't throw if the error occurred in loading the
603 // schema.
604 return;
605 }
606 throw e;
607 }
608 }
609
610 // Part of the backwards-compatibility behavior described above `start` to
611 // make ApolloServer work if you don't explicitly call `start`, as well as for
612 // serverless frameworks where there is no `start`. This is called at the
613 // beginning of each GraphQL request by `graphQLServerOptions`. It calls
614 // `start` for you if it hasn't been called yet, and only returns successfully
615 // if some call to `start` succeeds.
616 //
617 // This function assumes it is being called in a context where any error it
618 // throws may be shown to the end user, so it only throws specific errors
619 // without details. If it's throwing due to a startup error, it will log that
620 // error each time it is called before throwing a redacted error.
621 private async ensureStarted(): Promise<SchemaDerivedData> {
622 while (true) {
623 switch (this.state.phase) {
624 case 'initialized with gateway':
625 case 'initialized with schema':
626 try {
627 await this._start();
628 } catch {
629 // Any thrown error should transition us to 'failed to start', and
630 // we'll handle that on the next iteration of the while loop.
631 }
632 // continue the while loop
633 break;
634 case 'starting':
635 case 'invoking serverWillStart':
636 await this.state.barrier;
637 // continue the while loop
638 break;
639 case 'failed to start':
640 // First we log the error that prevented startup (which means it will
641 // get logged once for every GraphQL operation).
642 this.logStartupError(this.state.error);
643 // Now make the operation itself fail.
644 // We intentionally do not re-throw actual startup error as it may contain
645 // implementation details and this error will propagate to the client.
646 throw new Error(
647 'This data graph is missing a valid configuration. More details may be available in the server logs.',
648 );
649 case 'started':
650 return this.state.schemaDerivedData;
651 case 'stopping':
652 throw new Error(
653 'Cannot execute GraphQL operations while the server is stopping.',
654 );
655 case 'stopped':
656 throw new Error(
657 'Cannot execute GraphQL operations after the server has stopped.',
658 );
659 default:
660 throw new UnreachableCaseError(this.state);
661 }
662 }
663 }
664
665 // Part of the backwards-compatibility behavior described above `start` to
666 // make ApolloServer work if you don't explicitly call `start`. This is called
667 // by some of the integration frameworks when you interact with them (eg by
668 // calling applyMiddleware). It is also called from the end of the constructor
669 // for serverless framework integrations.
670 //
671 // It calls `start` for you if it hasn't been called yet, but doesn't wait for
672 // `start` to finish. The goal is that if you don't call `start` yourself the
673 // server should still do the rest of startup vaguely near when your server
674 // starts, not just when the first GraphQL request comes in. Without this
675 // call, startup wouldn't occur until `graphQLServerOptions` invokes
676 // `ensureStarted`.
677 protected ensureStarting() {
678 if (
679 this.state.phase === 'initialized with gateway' ||
680 this.state.phase === 'initialized with schema'
681 ) {
682 // Ah well. It would have been nice if the user had bothered
683 // to call and await `start()`; that way they'd be able to learn
684 // about any errors from it. Instead we'll kick it off here.
685 // Any thrown error will get logged, and also will cause
686 // every call to ensureStarted (ie, every GraphQL operation)
687 // to log it again and prevent the operation from running.
688 this._start().catch((e) => this.logStartupError(e));
689 }
690 }
691
692 // Given an error that occurred during Apollo Server startup, log it with a
693 // helpful message. Note that this is only used if `ensureStarting` or
694 // `ensureStarted` had to initiate the startup process; if you call
695 // `start` yourself (or you're using `apollo-server` whose `listen()` does
696 // it for you) then you can handle the error however you'd like rather than
697 // this log occurring. (We don't suggest the use of `start()` for serverless
698 // frameworks because they don't support it.)
699 private logStartupError(err: Error) {
700 const prelude = this.serverlessFramework()
701 ? 'An error occurred during Apollo Server startup.'
702 : 'Apollo Server was started implicitly and an error occurred during startup. ' +
703 '(Consider calling `await server.start()` immediately after ' +
704 '`server = new ApolloServer()` so you can handle these errors directly before ' +
705 'starting your web server.)';
706 this.logger.error(
707 prelude +
708 ' All GraphQL requests will now fail. The startup error ' +
709 'was: ' +
710 ((err && err.message) || err),
711 );
712 }
713
714 private async startGatewayAndLoadSchema(
715 gateway: GraphQLService,
716 ): Promise<GraphQLSchema> {
717 // Store the unsubscribe handles, which are returned from
718 // `onSchemaChange`, for later disposal when the server stops
719 const unsubscriber = gateway.onSchemaChange((schema) => {
720 // If we're still happily running, update our schema-derived state.
721 if (this.state.phase === 'started') {
722 this.state.schemaDerivedData = this.generateSchemaDerivedData(schema);
723 }
724 });
725 this.toDispose.add(async () => unsubscriber());
726
727 // For backwards compatibility with old versions of @apollo/gateway.
728 const engineConfig =
729 this.apolloConfig.keyHash && this.apolloConfig.graphId
730 ? {
731 apiKeyHash: this.apolloConfig.keyHash,
732 graphId: this.apolloConfig.graphId,
733 graphVariant: this.apolloConfig.graphVariant,
734 }
735 : undefined;
736
737 const config = await gateway.load({
738 apollo: this.apolloConfig,
739 engine: engineConfig,
740 });
741 this.toDispose.add(async () => await gateway.stop?.());
742 return config.schema;
743 }
744
745 private constructSchema(): GraphQLSchema {
746 const {
747 schema,
748 modules,
749 typeDefs,
750 resolvers,
751 schemaDirectives,
752 parseOptions,
753 } = this.config;
754 if (schema) {
755 return schema;
756 }
757
758 if (modules) {
759 const { schema, errors } = buildServiceDefinition(modules);
760 if (errors && errors.length > 0) {
761 throw new Error(errors.map((error) => error.message).join('\n\n'));
762 }
763 return schema!;
764 }
765
766 if (!typeDefs) {
767 throw Error(
768 'Apollo Server requires either an existing schema, modules or typeDefs',
769 );
770 }
771
772 const augmentedTypeDefs = Array.isArray(typeDefs) ? typeDefs : [typeDefs];
773
774 // We augment the typeDefs with the @cacheControl directive and associated
775 // scope enum, so makeExecutableSchema won't fail SDL validation
776
777 if (!isDirectiveDefined(augmentedTypeDefs, 'cacheControl')) {
778 augmentedTypeDefs.push(
779 gql`
780 enum CacheControlScope {
781 PUBLIC
782 PRIVATE
783 }
784
785 directive @cacheControl(
786 maxAge: Int
787 scope: CacheControlScope
788 ) on FIELD_DEFINITION | OBJECT | INTERFACE
789 `,
790 );
791 }
792
793 if (this.uploadsConfig) {
794 const { GraphQLUpload } = require('@apollographql/graphql-upload-8-fork');
795 if (Array.isArray(resolvers)) {
796 if (resolvers.every((resolver) => !resolver.Upload)) {
797 resolvers.push({ Upload: GraphQLUpload });
798 }
799 } else {
800 if (resolvers && !resolvers.Upload) {
801 resolvers.Upload = GraphQLUpload;
802 }
803 }
804
805 // We augment the typeDefs with the Upload scalar, so typeDefs that
806 // don't include it won't fail
807 augmentedTypeDefs.push(
808 gql`
809 scalar Upload
810 `,
811 );
812 }
813
814 return makeExecutableSchema({
815 typeDefs: augmentedTypeDefs,
816 schemaDirectives,
817 resolvers,
818 parseOptions,
819 });
820 }
821
822 private generateSchemaDerivedData(schema: GraphQLSchema): SchemaDerivedData {
823 const schemaHash = generateSchemaHash(schema!);
824
825 const { mocks, mockEntireSchema, extensions: _extensions } = this.config;
826
827 if (mocks || (typeof mockEntireSchema !== 'undefined' && mocks !== false)) {
828 addMockFunctionsToSchema({
829 schema,
830 mocks:
831 typeof mocks === 'boolean' || typeof mocks === 'undefined'
832 ? {}
833 : mocks,
834 preserveResolvers:
835 typeof mockEntireSchema === 'undefined' ? false : !mockEntireSchema,
836 });
837 }
838
839 const extensions = [];
840
841 // Note: doRunQuery will add its own extensions if you set tracing,
842 // or cacheControl.
843 extensions.push(...(_extensions || []));
844
845 // Initialize the document store. This cannot currently be disabled.
846 const documentStore = this.initializeDocumentStore();
847
848 return {
849 schema,
850 schemaHash,
851 extensions,
852 documentStore,
853 };
854 }
855
856 public async stop() {
857 // Calling stop more than once should have the same result as the first time.
858 if (this.state.phase === 'stopped') {
859 if (this.state.stopError) {
860 throw this.state.stopError;
861 }
862 return;
863 }
864
865 // Two parallel calls to stop; just wait for the other one to finish and
866 // do whatever it did.
867 if (this.state.phase === 'stopping') {
868 await this.state.barrier;
869 // The cast here is because TS doesn't understand that this.state can
870 // change during the await
871 // (https://github.com/microsoft/TypeScript/issues/9998).
872 const state = this.state as ServerState;
873 if (state.phase !== 'stopped') {
874 throw Error(`Surprising post-stopping state ${state.phase}`);
875 }
876 if (state.stopError) {
877 throw state.stopError;
878 }
879 return;
880 }
881
882 // Commit to stopping, actually stop, and update the phase.
883 this.state = { phase: 'stopping', barrier: resolvable() };
884 try {
885 // We run shutdown handlers in two phases because we don't want to turn
886 // off our signal listeners until we've done the important parts of shutdown
887 // like running serverWillStop handlers. (We can make this more generic later
888 // if it's helpful.)
889 await Promise.all([...this.toDispose].map((dispose) => dispose()));
890 if (this.subscriptionServer) this.subscriptionServer.close();
891 await Promise.all([...this.toDisposeLast].map((dispose) => dispose()));
892 } catch (stopError) {
893 this.state = { phase: 'stopped', stopError };
894 return;
895 }
896 this.state = { phase: 'stopped', stopError: null };
897 }
898
899 public installSubscriptionHandlers(
900 server:
901 | HttpServer
902 | HttpsServer
903 | Http2Server
904 | Http2SecureServer
905 | WebSocket.Server,
906 ) {
907 if (!this.subscriptionServerOptions) {
908 if (this.config.gateway) {
909 throw Error(
910 'Subscriptions are not supported when operating as a gateway',
911 );
912 }
913 if (this.supportsSubscriptions()) {
914 throw Error(
915 'Subscriptions are disabled, due to subscriptions set to false in the ApolloServer constructor',
916 );
917 } else {
918 throw Error(
919 'Subscriptions are not supported, choose an integration, such as apollo-server-express that allows persistent connections',
920 );
921 }
922 }
923 const { SubscriptionServer } = require('subscriptions-transport-ws');
924 const {
925 onDisconnect,
926 onConnect,
927 keepAlive,
928 path,
929 } = this.subscriptionServerOptions;
930
931 let schema: GraphQLSchema;
932 switch (this.state.phase) {
933 case 'initialized with schema':
934 case 'invoking serverWillStart':
935 case 'started':
936 schema = this.state.schemaDerivedData.schema;
937 break;
938 case 'initialized with gateway':
939 // shouldn't happen: gateway doesn't support subs
940 case 'starting':
941 // shouldn't happen: there's no await between 'starting' and
942 // 'invoking serverWillStart' without gateway
943 case 'failed to start':
944 // only happens if you call 'start' yourself, in which case you really
945 // ought to see what happens before calling this function
946 case 'stopping':
947 case 'stopped':
948 // stopping is unlikely to happen during startup
949 throw new Error(
950 `Can't install subscription handlers when state is ${this.state.phase}`,
951 );
952 default:
953 throw new UnreachableCaseError(this.state);
954 }
955
956 this.subscriptionServer = SubscriptionServer.create(
957 {
958 schema,
959 execute,
960 subscribe,
961 onConnect: onConnect
962 ? onConnect
963 : (connectionParams: Object) => ({ ...connectionParams }),
964 onDisconnect: onDisconnect,
965 onOperation: async (
966 message: { payload: any },
967 connection: ExecutionParams,
968 ) => {
969 connection.formatResponse = (value: ExecutionResult) => ({
970 ...value,
971 errors:
972 value.errors &&
973 formatApolloErrors([...value.errors], {
974 formatter: this.requestOptions.formatError,
975 debug: this.requestOptions.debug,
976 }),
977 });
978
979 connection.formatError = this.requestOptions.formatError;
980
981 let context: Context = this.context ? this.context : { connection };
982
983 try {
984 context =
985 typeof this.context === 'function'
986 ? await this.context({ connection, payload: message.payload })
987 : context;
988 } catch (e) {
989 throw formatApolloErrors([e], {
990 formatter: this.requestOptions.formatError,
991 debug: this.requestOptions.debug,
992 })[0];
993 }
994
995 return { ...connection, context };
996 },
997 keepAlive,
998 validationRules: this.requestOptions.validationRules,
999 },
1000 server instanceof NetServer || server instanceof TlsServer
1001 ? {
1002 server,
1003 path,
1004 }
1005 : server,
1006 );
1007 }
1008
1009 protected supportsSubscriptions(): boolean {
1010 return false;
1011 }
1012
1013 protected supportsUploads(): boolean {
1014 return false;
1015 }
1016
1017 protected serverlessFramework(): boolean {
1018 return false;
1019 }
1020
1021 private ensurePluginInstantiation(plugins: PluginDefinition[] = []): void {
1022 const pluginsToInit: PluginDefinition[] = [];
1023
1024 // Internal plugins should be added to `pluginsToInit` here.
1025 // User's plugins, provided as an argument to this method, will be added
1026 // at the end of that list so they take precedence.
1027
1028 // If the user has enabled it explicitly, add our tracing plugin.
1029 // (This is the plugin which adds a verbose JSON trace to every GraphQL response;
1030 // it was designed for use with the obsolete engineproxy, and also works
1031 // with a graphql-playground trace viewer, but isn't generally recommended
1032 // (eg, it really does send traces with every single request). The newer
1033 // inline tracing plugin may be what you want, or just usage reporting if
1034 // the goal is to get traces to Apollo's servers.)
1035 if (this.config.tracing) {
1036 pluginsToInit.push(pluginTracing());
1037 }
1038
1039 // Enable cache control unless it was explicitly disabled.
1040 if (this.config.cacheControl !== false) {
1041 let cacheControlOptions: CacheControlExtensionOptions = {};
1042 if (
1043 typeof this.config.cacheControl === 'boolean' &&
1044 this.config.cacheControl === true
1045 ) {
1046 // cacheControl: true means that the user needs the cache-control
1047 // extensions. This means we are running the proxy, so we should not
1048 // strip out the cache control extension and not add cache-control headers
1049 cacheControlOptions = {
1050 stripFormattedExtensions: false,
1051 calculateHttpHeaders: false,
1052 defaultMaxAge: 0,
1053 };
1054 } else {
1055 // Default behavior is to run default header calculation and return
1056 // no cacheControl extensions
1057 cacheControlOptions = {
1058 stripFormattedExtensions: true,
1059 calculateHttpHeaders: true,
1060 defaultMaxAge: 0,
1061 ...this.config.cacheControl,
1062 };
1063 }
1064
1065 pluginsToInit.push(pluginCacheControl(cacheControlOptions));
1066 }
1067
1068 pluginsToInit.push(...plugins);
1069
1070 this.plugins = pluginsToInit.map((plugin) => {
1071 if (typeof plugin === 'function') {
1072 return plugin();
1073 }
1074 return plugin;
1075 });
1076
1077 const alreadyHavePluginWithInternalId = (id: InternalPluginId) =>
1078 this.plugins.some(
1079 (p) => pluginIsInternal(p) && p.__internal_plugin_id__() === id,
1080 );
1081
1082 // Special case: usage reporting is on by default if you configure an API key.
1083 {
1084 const alreadyHavePlugin = alreadyHavePluginWithInternalId(
1085 'UsageReporting',
1086 );
1087 const { engine } = this.config;
1088 const disabledViaLegacyOption =
1089 engine === false ||
1090 (typeof engine === 'object' && engine.reportTiming === false);
1091 if (alreadyHavePlugin) {
1092 if (engine !== undefined) {
1093 throw Error(
1094 "You can't combine the legacy `new ApolloServer({engine})` option with directly " +
1095 'creating an ApolloServerPluginUsageReporting plugin. See ' +
1096 'https://go.apollo.dev/s/migration-engine-plugins',
1097 );
1098 }
1099 } else if (this.apolloConfig.key && !disabledViaLegacyOption) {
1100 // Keep this plugin first so it wraps everything. (Unfortunately despite
1101 // the fact that the person who wrote this line also was the original
1102 // author of the comment above in #1105, they don't quite understand why this was important.)
1103 this.plugins.unshift(
1104 typeof engine === 'object'
1105 ? ApolloServerPluginUsageReportingFromLegacyOptions(engine)
1106 : ApolloServerPluginUsageReporting(),
1107 );
1108 }
1109 }
1110
1111 // Special case: schema reporting can be turned on via environment variable.
1112 {
1113 const alreadyHavePlugin = alreadyHavePluginWithInternalId(
1114 'SchemaReporting',
1115 );
1116 const enabledViaEnvVar = process.env.APOLLO_SCHEMA_REPORTING === 'true';
1117 const { engine } = this.config;
1118 const enabledViaLegacyOption =
1119 typeof engine === 'object' &&
1120 (engine.reportSchema || engine.experimental_schemaReporting);
1121 if (alreadyHavePlugin || enabledViaEnvVar || enabledViaLegacyOption) {
1122 if (this.config.gateway) {
1123 throw new Error(
1124 [
1125 "Schema reporting is not yet compatible with the gateway. If you're",
1126 'interested in using schema reporting with the gateway, please',
1127 'contact Apollo support. To set up managed federation, see',
1128 'https://go.apollo.dev/s/managed-federation',
1129 ].join(' '),
1130 );
1131 }
1132 }
1133 if (alreadyHavePlugin) {
1134 if (engine !== undefined) {
1135 throw Error(
1136 "You can't combine the legacy `new ApolloServer({engine})` option with directly " +
1137 'creating an ApolloServerPluginSchemaReporting plugin. See ' +
1138 'https://go.apollo.dev/s/migration-engine-plugins',
1139 );
1140 }
1141 } else if (!this.apolloConfig.key) {
1142 if (enabledViaEnvVar) {
1143 throw new Error(
1144 "You've enabled schema reporting by setting the APOLLO_SCHEMA_REPORTING " +
1145 'environment variable to true, but you also need to provide your ' +
1146 'Apollo API key, via the APOLLO_KEY environment ' +
1147 'variable or via `new ApolloServer({apollo: {key})',
1148 );
1149 }
1150 if (enabledViaLegacyOption) {
1151 throw new Error(
1152 "You've enabled schema reporting in the `engine` argument to `new ApolloServer()`, " +
1153 'but you also need to provide your Apollo API key, via the APOLLO_KEY environment ' +
1154 'variable or via `new ApolloServer({apollo: {key})',
1155 );
1156 }
1157 } else if (enabledViaEnvVar || enabledViaLegacyOption) {
1158 const options: ApolloServerPluginSchemaReportingOptions = {};
1159 if (typeof engine === 'object') {
1160 options.initialDelayMaxMs =
1161 engine.schemaReportingInitialDelayMaxMs ??
1162 engine.experimental_schemaReportingInitialDelayMaxMs;
1163 options.overrideReportedSchema =
1164 engine.overrideReportedSchema ??
1165 engine.experimental_overrideReportedSchema;
1166 options.endpointUrl = engine.schemaReportingUrl;
1167 }
1168 this.plugins.push(ApolloServerPluginSchemaReporting(options));
1169 }
1170 }
1171
1172 // Special case: inline tracing is on by default for federated schemas.
1173 {
1174 const alreadyHavePlugin = alreadyHavePluginWithInternalId('InlineTrace');
1175 const { engine } = this.config;
1176 if (alreadyHavePlugin) {
1177 if (engine !== undefined) {
1178 throw Error(
1179 "You can't combine the legacy `new ApolloServer({engine})` option with directly " +
1180 'creating an ApolloServerPluginInlineTrace plugin. See ' +
1181 'https://go.apollo.dev/s/migration-engine-plugins',
1182 );
1183 }
1184 } else if (this.config.engine !== false) {
1185 // If we haven't explicitly disabled inline tracing via
1186 // ApolloServerPluginInlineTraceDisabled or engine:false,
1187 // we set up inline tracing in "only if federated" mode.
1188 // (This is slightly different than the pre-ApolloServerPluginInlineTrace where
1189 // we would also avoid doing this if an API key was configured and log a warning.)
1190 const options: ApolloServerPluginInlineTraceOptions = {
1191 __onlyIfSchemaIsFederated: true,
1192 };
1193 if (typeof engine === 'object') {
1194 options.rewriteError = engine.rewriteError;
1195 }
1196 this.plugins.push(ApolloServerPluginInlineTrace(options));
1197 }
1198 }
1199 }
1200
1201 private initializeDocumentStore(): InMemoryLRUCache<DocumentNode> {
1202 return new InMemoryLRUCache<DocumentNode>({
1203 // Create ~about~ a 30MiB InMemoryLRUCache. This is less than precise
1204 // since the technique to calculate the size of a DocumentNode is
1205 // only using JSON.stringify on the DocumentNode (and thus doesn't account
1206 // for unicode characters, etc.), but it should do a reasonable job at
1207 // providing a caching document store for most operations.
1208 maxSize:
1209 Math.pow(2, 20) * (this.experimental_approximateDocumentStoreMiB || 30),
1210 sizeCalculator: approximateObjectSize,
1211 });
1212 }
1213
1214 // This function is used by the integrations to generate the graphQLOptions
1215 // from an object containing the request and other integration specific
1216 // options
1217 protected async graphQLServerOptions(
1218 integrationContextArgument?: Record<string, any>,
1219 ): Promise<GraphQLServerOptions> {
1220 const {
1221 schema,
1222 schemaHash,
1223 documentStore,
1224 extensions,
1225 } = await this.ensureStarted();
1226
1227 let context: Context = this.context ? this.context : {};
1228
1229 try {
1230 context =
1231 typeof this.context === 'function'
1232 ? await this.context(integrationContextArgument || {})
1233 : context;
1234 } catch (error) {
1235 // Defer context error resolution to inside of runQuery
1236 context = () => {
1237 throw error;
1238 };
1239 }
1240
1241 return {
1242 schema,
1243 schemaHash,
1244 logger: this.logger,
1245 plugins: this.plugins,
1246 documentStore,
1247 extensions,
1248 context,
1249 // Allow overrides from options. Be explicit about a couple of them to
1250 // avoid a bad side effect of the otherwise useful noUnusedLocals option
1251 // (https://github.com/Microsoft/TypeScript/issues/21673).
1252 persistedQueries: this.requestOptions
1253 .persistedQueries as PersistedQueryOptions,
1254 fieldResolver: this.requestOptions.fieldResolver as GraphQLFieldResolver<
1255 any,
1256 any
1257 >,
1258 parseOptions: this.parseOptions,
1259 ...this.requestOptions,
1260 };
1261 }
1262
1263 public async executeOperation(request: GraphQLRequest) {
1264 const options = await this.graphQLServerOptions();
1265
1266 if (typeof options.context === 'function') {
1267 options.context = (options.context as () => never)();
1268 } else if (typeof options.context === 'object') {
1269 // FIXME: We currently shallow clone the context for every request,
1270 // but that's unlikely to be what people want.
1271 // We allow passing in a function for `context` to ApolloServer,
1272 // but this only runs once for a batched request (because this is resolved
1273 // in ApolloServer#graphQLServerOptions, before runHttpQuery is invoked).
1274 // NOTE: THIS IS DUPLICATED IN runHttpQuery.ts' buildRequestContext.
1275 options.context = cloneObject(options.context);
1276 }
1277
1278 const requestCtx: GraphQLRequestContext = {
1279 logger: this.logger,
1280 schema: options.schema,
1281 schemaHash: options.schemaHash,
1282 request,
1283 context: options.context || Object.create(null),
1284 cache: options.cache!,
1285 metrics: {},
1286 response: {
1287 http: {
1288 headers: new Headers(),
1289 },
1290 },
1291 debug: options.debug,
1292 };
1293
1294 return processGraphQLRequest(options, requestCtx);
1295 }
1296}
1297
1298function printNodeFileUploadsMessage(logger: Logger) {
1299 logger.error(
1300 [
1301 '*****************************************************************',
1302 '* *',
1303 '* ERROR! Manual intervention is necessary for Node.js < v8.5.0! *',
1304 '* *',
1305 '*****************************************************************',
1306 '',
1307 'The third-party `graphql-upload` package, which is used to implement',
1308 'file uploads in Apollo Server 2.x, no longer supports Node.js LTS',
1309 'versions prior to Node.js v8.5.0.',
1310 '',
1311 'Deployments which NEED file upload capabilities should update to',
1312 'Node.js >= v8.5.0 to continue using uploads.',
1313 '',
1314 'If this server DOES NOT NEED file uploads and wishes to continue',
1315 'using this version of Node.js, uploads can be disabled by adding:',
1316 '',
1317 ' uploads: false,',
1318 '',
1319 '...to the options for Apollo Server and re-deploying the server.',
1320 '',
1321 'For more information, see https://bit.ly/gql-upload-node-6.',
1322 '',
1323 ].join('\n'),
1324 );
1325}
1326
\No newline at end of file