Skip to content

Commit 9c38dcc

Browse files
authored
Make server.start() required for non-serverless frameworks (#5195)
AS v2.22 introduced the method `server.start()` which could be awaited immediately after `new ApolloServer()` in order to catch startup errors (for example, errors loading schema from gateway). This method exists only for non-serverless framework integrations. For `apollo-server` its semantics are integrated into `server.listen()` and for serverless frameworks you typically must set up the handler synchronously. In AS2 this method was optional. This PR makes the method required. So instead of the framework integration kicking off a `start` in the background if you don't do it yourself, it just throws an error telling you to call start. Fixes #5050.
1 parent 9e3218d commit 9c38dcc

File tree

31 files changed

+267
-315
lines changed

31 files changed

+267
-315
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The version headers in this history reflect the versions of Apollo Server itself
2828
- `apollo-server-lambda`: The handler returned by `createHandler` can now only be called as an async function returning a `Promise` (it no longer optionally accepts a callback as the third argument). All current Lambda Node runtimes support this invocation mode (so `exports.handler = server.createHandler()` will keep working without any changes), but if you've written your own handler which calls the handler returned by `createHandler` with a callback, you'll need to handle its `Promise` return value instead.
2929
- The `tracing` option to `new ApolloServer` has been removed, and the `apollo-server-tracing` package has been deprecated and is no longer being published. This package implemented an inefficient JSON format for execution traces returned on the `tracing` GraphQL response extension; it was only consumed by the deprecated `engineproxy` and Playground. If you really need this format, the old version of `apollo-server-tracing` should still work (`new ApolloServer({plugins: [require('apollo-server-tracing').plugin()]})`).
3030
- The `cacheControl` option to `new ApolloServer` has been removed. The functionality provided by `cacheControl: true` or `cacheControl: {stripFormattedExtensions: false}` (which included a `cacheControl` extension in the GraphQL response, for use by the deprecated `engineproxy`) has been entirely removed. The default behavior of Apollo Server continues to be calculating an overall cache policy and setting the `Cache-Control` HTTP header, but this is now implemented directly inside `apollo-server-core` rather than a separate `apollo-cache-control` package (this package has been deprecated and is no longer being published). Tweaking cache control settings like `defaultMaxAge` is now done via the newly exported `ApolloServerPluginCacheControl` plugin rather than as a top-level constructor option. This follows the same pattern as the other built-in plugins like usage reporting. The `CacheHint` and `CacheScope` types are now exported from `apollo-server-types`.
31+
- When using a non-serverless framework integration (Express, Fastify, Hapi, Koa, Micro, or Cloudflare), you now *must* `await server.start()` before attaching the server to your framework. (This method was introduced in v2.22 but was optional before Apollo Server 3.) This does not apply to the batteries-included `apollo-server` or to serverless framework integrations.
3132
- Top-level exports have changed. E.g.,
3233

3334
- We no longer re-export the entirety of `graphql-tools` (including `makeExecutableSchema`) from all Apollo Server packages. If you'd like to continue using them, install [`graphql-tools`](https://www.graphql-tools.com/) or one of its sub-packages yourself.

docs/source/api/apollo-server.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -659,19 +659,15 @@ The async `start` method instructs Apollo Server to prepare to handle incoming o
659659
660660
Always call `await server.start()` *before* calling `server.applyMiddleware` and starting your HTTP server. This allows you to react to Apollo Server startup failures by crashing your process instead of starting to serve traffic.
661661

662+
This method was optional in Apollo Server 2 but is required in Apollo Server 3.
663+
662664
##### Triggered actions
663665

664666
The `start` method triggers the following actions:
665667

666668
1. If your server is a [federated gateway](https://www.apollographql.com/docs/federation/managed-federation/overview/), it attempts to fetch its schema. If the fetch fails, `start` throws an error.
667669
2. Your server calls all of the [`serverWillStart` handlers](../integrations/plugins/#serverwillstart) of your installed plugins. If any of these handlers throw an error, `start` throws an error.
668670

669-
##### Backward compatibility
670-
671-
To ensure backward compatibility, calling `await server.start()` is optional. If you don't call it yourself, your integration package invokes it when you call `server.applyMiddleware`. Incoming GraphQL operations wait to execute until Apollo Server has started, and those operations fail if startup fails (a redacted error message is sent to the GraphQL client).
672-
673-
We recommend calling `await server.start()` yourself, so that your web server doesn't start accepting GraphQL requests until Apollo Server is ready to process them.
674-
675671
#### `applyMiddleware`
676672

677673
Connects Apollo Server to the HTTP framework of a Node.js middleware library, such as hapi or express.

packages/apollo-server-azure-functions/src/__tests__/azureFunctionApollo.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Config } from 'apollo-server-core';
77
import url from 'url';
88
import { IncomingMessage, ServerResponse } from 'http';
99

10-
const createAzureFunction = (options: CreateAppOptions = {}) => {
10+
const createAzureFunction = async (options: CreateAppOptions = {}) => {
1111
const server = new ApolloServer(
1212
(options.graphqlOptions as Config) || { schema: Schema },
1313
);
@@ -51,7 +51,7 @@ const createAzureFunction = (options: CreateAppOptions = {}) => {
5151
};
5252

5353
describe('integration:AzureFunctions', () => {
54-
testSuite(createAzureFunction);
54+
testSuite({createApp: createAzureFunction, serverlessFramework: true});
5555

5656
it('can append CORS headers to GET request', async () => {
5757
const server = new ApolloServer({ schema: Schema });

packages/apollo-server-cloud-functions/src/__tests__/googleCloudApollo.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const simulateGcfMiddleware = (
2626
next();
2727
};
2828

29-
const createCloudFunction = (options: CreateAppOptions = {}) => {
29+
const createCloudFunction = async (options: CreateAppOptions = {}) => {
3030
const handler = new ApolloServer(
3131
(options.graphqlOptions as Config) || { schema: Schema },
3232
).createHandler();
@@ -49,5 +49,5 @@ describe('googleCloudApollo', () => {
4949
});
5050

5151
describe('integration:CloudFunction', () => {
52-
testSuite(createCloudFunction);
52+
testSuite({createApp: createCloudFunction, serverlessFramework: true});
5353
});

packages/apollo-server-cloudflare/src/ApolloServer.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ export class ApolloServer extends ApolloServerBase {
1414
}
1515

1616
public async listen() {
17-
// In case the user didn't bother to call and await the `start` method, we
18-
// kick it off in the background (with any errors getting logged
19-
// and also rethrown from graphQLServerOptions during later requests).
20-
this.ensureStarting();
17+
this.assertStarted('listen');
2118

2219
addEventListener('fetch', (event: FetchEvent) => {
2320
event.respondWith(

packages/apollo-server-core/src/ApolloServer.ts

Lines changed: 46 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,8 @@ export class ApolloServerBase {
274274
if (gateway) {
275275
// ApolloServer has been initialized but we have not yet tried to load the
276276
// schema from the gateway. That will wait until the user calls
277-
// `server.start()`, or until `ensureStarting` or `ensureStarted` are
278-
// called. (In the case of a serverless framework integration,
279-
// `ensureStarting` is automatically called at the end of the
280-
// constructor.)
277+
// `server.start()` or `server.listen()`, or (in serverless frameworks)
278+
// until the `this._start()` call at the end of this constructor.
281279
this.state = { phase: 'initialized with gateway', gateway };
282280

283281
// The main thing that the Gateway does is replace execution with
@@ -306,12 +304,12 @@ export class ApolloServerBase {
306304
// unlike (eg) applyMiddleware, so we can't expect you to `await
307305
// server.start()` before calling it. So we kick off the start
308306
// asynchronously from the constructor, and failures are logged and cause
309-
// later requests to fail (in ensureStarted, called by
310-
// graphQLServerOptions). There's no way to make "the whole server fail"
307+
// later requests to fail (in `ensureStarted`, called by
308+
// `graphQLServerOptions`). There's no way to make "the whole server fail"
311309
// separately from making individual requests fail, but that's not entirely
312310
// unreasonable for a "serverless" model.
313311
if (this.serverlessFramework()) {
314-
this.ensureStarting();
312+
this._start().catch((e) => this.logStartupError(e));
315313
}
316314
}
317315

@@ -324,35 +322,22 @@ export class ApolloServerBase {
324322
// `listen` method takes care of that for you (this is why the actual logic is
325323
// in the `_start` helper).
326324
//
327-
// If instead you're using an integration package, you are highly encouraged
328-
// to await a call to `start` immediately after creating your `ApolloServer`,
329-
// before attaching it to your web framework and starting to accept requests.
330-
// `start` should only be called once; if it throws and you'd like to retry,
331-
// just create another `ApolloServer`. (Note that this paragraph does not
332-
// apply to "serverless framework" integrations like Lambda.)
333-
//
334-
// For backwards compatibility with the pre-2.22 API, you are not required to
335-
// call start() yourself (this may change in AS3). Most integration packages
336-
// call the protected `ensureStarting` when you first interact with them,
337-
// which kicks off a "background" call to `start` if you haven't called it
338-
// yourself. Then `graphQLServerOptions` (which is called before processing)
339-
// each incoming GraphQL request) calls `ensureStarted` which waits for
340-
// `start` to successfully complete (possibly by calling it itself), and
341-
// throws a redacted error if `start` was not successful. If `start` is
342-
// invoked implicitly by either of these mechanisms, any error that it throws
343-
// will be logged when they occur and then again on every subsequent
344-
// `graphQLServerOptions` call (ie, every GraphQL request). Note that start
345-
// failures are not recoverable without creating a new ApolloServer. You are
346-
// highly encouraged to make these backwards-compatibility paths into no-ops
347-
// by awaiting a call to `start` yourself.
325+
// If instead you're using an integration package for a non-serverless
326+
// framework (like Express), you must await a call to `start` immediately
327+
// after creating your `ApolloServer`, before attaching it to your web
328+
// framework and starting to accept requests. `start` should only be called
329+
// once; if it throws and you'd like to retry, just create another
330+
// `ApolloServer`. (Calling `start` was optional in Apollo Server 2, but in
331+
// Apollo Server 3 the methods like `server.applyMiddleware` use
332+
// `assertStarted` to throw if `start` hasn't successfully completed.)
348333
//
349334
// Serverless integrations like Lambda (which override `serverlessFramework()`
350335
// to return true) do not support calling `start()`, because their lifecycle
351336
// doesn't allow you to wait before assigning a handler or allowing the
352-
// handler to be called. So they call `ensureStarting` at the end of the
337+
// handler to be called. So they call `_start()` at the end of the
353338
// constructor, and don't really differentiate between startup failures and
354339
// request failures. This is hopefully appropriate for a "serverless"
355-
// framework. As above, startup failures result in returning a redacted error
340+
// framework. Serverless startup failures result in returning a redacted error
356341
// to the end user and logging the more detailed error.
357342
public async start(): Promise<void> {
358343
if (this.serverlessFramework()) {
@@ -443,55 +428,25 @@ export class ApolloServerBase {
443428
}
444429
}
445430

446-
/**
447-
* @deprecated This deprecated method is provided for backwards compatibility
448-
* with the pre-v2.22 API. It was sort of a combination of the v2.22 APIs
449-
* `ensureStarting` and `start`; it was generally called "in the background"
450-
* by integrations to kick off the start process (like `ensureStarting`) and
451-
* then the Promise it returns was awaited later before running operations
452-
* (sort of like `start`). It had odd error handling semantics, in that it
453-
* would ignore any error that came from loading the schema, but would throw
454-
* errors that came from `serverWillStart`.
455-
*
456-
* We keep it around for backwards-compatibility with pre-v2.22 integrations,
457-
* though we just make it call `ensureStarting`. This does mean that the part
458-
* of the integration which awaits its result doesn't actually await anything
459-
* interesting (despite being async, the method itself doesn't await
460-
* anything), but since executing operations now calls `ensureStarted`, that's
461-
* OK. (In v2.22.0 and v2.22.1 we tried to mimic the old `willStart` behavior
462-
* more closely which led to a bug where `start` could be invoked multiple
463-
* times. This approach is simpler.)
464-
*
465-
* Anyone calling this method should call `start` or `ensureStarting` instead.
466-
*/
467-
protected async willStart() {
468-
this.ensureStarting();
469-
}
470-
471-
// Part of the backwards-compatibility behavior described above `start` to
472-
// make ApolloServer work if you don't explicitly call `start`, as well as for
473-
// serverless frameworks where there is no `start`. This is called at the
474-
// beginning of each GraphQL request by `graphQLServerOptions`. It calls
475-
// `start` for you if it hasn't been called yet, and only returns successfully
476-
// if some call to `start` succeeds.
477-
//
478-
// This function assumes it is being called in a context where any error it
479-
// throws may be shown to the end user, so it only throws specific errors
480-
// without details. If it's throwing due to a startup error, it will log that
481-
// error each time it is called before throwing a redacted error.
431+
// This method is called at the beginning of each GraphQL request by
432+
// `graphQLServerOptions`. Most of its logic is only helpful for serverless
433+
// frameworks: unless you're in a serverless framework, you should have called
434+
// `await server.start()` before the server got to the point of running
435+
// GraphQL requests (`assertStarted` calls in the framework integrations
436+
// verify that) and so the only cases for non-serverless frameworks that this
437+
// should hit are 'started', 'stopping', and 'stopped'. For serverless
438+
// frameworks, this lets the server wait until fully started before serving
439+
// operations.
482440
private async ensureStarted(): Promise<SchemaDerivedData> {
483441
while (true) {
484442
switch (this.state.phase) {
485443
case 'initialized with gateway':
486444
case 'initialized with schema':
487-
try {
488-
await this._start();
489-
} catch {
490-
// Any thrown error should transition us to 'failed to start', and
491-
// we'll handle that on the next iteration of the while loop.
492-
}
493-
// continue the while loop
494-
break;
445+
// This error probably won't happen: serverless frameworks
446+
// automatically call `_start` at the end of the constructor, and
447+
// other frameworks call `assertStarted` before setting things up
448+
// enough to make calling this function possible.
449+
throw new Error("You need to call `server.start()` before using your Apollo Server.");
495450
case 'starting':
496451
case 'invoking serverWillStart':
497452
await this.state.barrier;
@@ -523,51 +478,26 @@ export class ApolloServerBase {
523478
}
524479
}
525480

526-
// Part of the backwards-compatibility behavior described above `start` to
527-
// make ApolloServer work if you don't explicitly call `start`. This is called
528-
// by some of the integration frameworks when you interact with them (eg by
529-
// calling applyMiddleware). It is also called from the end of the constructor
530-
// for serverless framework integrations.
531-
//
532-
// It calls `start` for you if it hasn't been called yet, but doesn't wait for
533-
// `start` to finish. The goal is that if you don't call `start` yourself the
534-
// server should still do the rest of startup vaguely near when your server
535-
// starts, not just when the first GraphQL request comes in. Without this
536-
// call, startup wouldn't occur until `graphQLServerOptions` invokes
537-
// `ensureStarted`.
538-
protected ensureStarting() {
539-
if (
540-
this.state.phase === 'initialized with gateway' ||
541-
this.state.phase === 'initialized with schema'
542-
) {
543-
// Ah well. It would have been nice if the user had bothered
544-
// to call and await `start()`; that way they'd be able to learn
545-
// about any errors from it. Instead we'll kick it off here.
546-
// Any thrown error will get logged, and also will cause
547-
// every call to ensureStarted (ie, every GraphQL operation)
548-
// to log it again and prevent the operation from running.
549-
this._start().catch((e) => this.logStartupError(e));
481+
protected assertStarted(methodName: string) {
482+
if (this.state.phase !== 'started') {
483+
throw new Error(
484+
'You must `await server.start()` before calling `server.' +
485+
methodName +
486+
'()`',
487+
);
550488
}
489+
// XXX do we need to do anything special for stopping/stopped?
551490
}
552491

553492
// Given an error that occurred during Apollo Server startup, log it with a
554-
// helpful message. Note that this is only used if `ensureStarting` or
555-
// `ensureStarted` had to initiate the startup process; if you call
556-
// `start` yourself (or you're using `apollo-server` whose `listen()` does
557-
// it for you) then you can handle the error however you'd like rather than
558-
// this log occurring. (We don't suggest the use of `start()` for serverless
559-
// frameworks because they don't support it.)
493+
// helpful message. This should only happen with serverless frameworks; with
494+
// other frameworks, you must `await server.start()` which will throw the
495+
// startup error directly instead of logging (or `await server.listen()` for
496+
// the batteries-included `apollo-server`).
560497
private logStartupError(err: Error) {
561-
const prelude = this.serverlessFramework()
562-
? 'An error occurred during Apollo Server startup.'
563-
: 'Apollo Server was started implicitly and an error occurred during startup. ' +
564-
'(Consider calling `await server.start()` immediately after ' +
565-
'`server = new ApolloServer()` so you can handle these errors directly before ' +
566-
'starting your web server.)';
567498
this.logger.error(
568-
prelude +
569-
' All GraphQL requests will now fail. The startup error ' +
570-
'was: ' +
499+
'An error occurred during Apollo Server startup. All GraphQL requests ' +
500+
'will now fail. The startup error was: ' +
571501
((err && err.message) || err),
572502
);
573503
}
@@ -704,7 +634,9 @@ export class ApolloServerBase {
704634
return false;
705635
}
706636

707-
private ensurePluginInstantiation(userPlugins: PluginDefinition[] = []): void {
637+
private ensurePluginInstantiation(
638+
userPlugins: PluginDefinition[] = [],
639+
): void {
708640
this.plugins = userPlugins.map((plugin) => {
709641
if (typeof plugin === 'function') {
710642
return plugin();

0 commit comments

Comments
 (0)