diff --git a/packages/apollo-server-cloud-function/src/ApolloServer.ts b/packages/apollo-server-cloud-function/src/ApolloServer.ts index 94e358e3e79..ea8ad6df2e2 100644 --- a/packages/apollo-server-cloud-function/src/ApolloServer.ts +++ b/packages/apollo-server-cloud-function/src/ApolloServer.ts @@ -43,6 +43,11 @@ export class ApolloServer extends ApolloServerBase { } public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { + // We will kick off the `willStart` event once for the server, and then + // await it before processing any requests by incorporating its `await` into + // the GraphQLServerOptions function which is called before each request. + const promiseWillStart = this.willStart(); + const corsHeaders = {} as Record; if (cors) { @@ -132,10 +137,17 @@ export class ApolloServer extends ApolloServerBase { res.set(corsHeaders); - graphqlCloudFunction(this.createGraphQLServerOptions.bind(this))( - req, - res, - ); + graphqlCloudFunction(async () => { + // In a world where this `createHandler` was async, we might avoid this + // but since we don't want to introduce a breaking change to this API + // (by switching it to `async`), we'll leverage the + // `GraphQLServerOptions`, which are dynamically built on each request, + // to `await` the `promiseWillStart` which we kicked off at the top of + // this method to ensure that it runs to completion (which is part of + // its contract) prior to processing the request. + await promiseWillStart; + return this.createGraphQLServerOptions(req, res); + })(req, res); }; } } diff --git a/packages/apollo-server-cloudflare/src/ApolloServer.ts b/packages/apollo-server-cloudflare/src/ApolloServer.ts index bb2ce9fe49a..ebf32449992 100644 --- a/packages/apollo-server-cloudflare/src/ApolloServer.ts +++ b/packages/apollo-server-cloudflare/src/ApolloServer.ts @@ -14,6 +14,7 @@ export class ApolloServer extends ApolloServerBase { } public async listen() { + await this.willStart(); const graphql = this.createGraphQLServerOptions.bind(this); addEventListener('fetch', (event: FetchEvent) => { event.respondWith(graphqlCloudflare(graphql)(event.request)); diff --git a/packages/apollo-server-express/src/ApolloServer.ts b/packages/apollo-server-express/src/ApolloServer.ts index 2205414cb21..d066f0a9e29 100644 --- a/packages/apollo-server-express/src/ApolloServer.ts +++ b/packages/apollo-server-express/src/ApolloServer.ts @@ -84,6 +84,9 @@ export class ApolloServer extends ApolloServerBase { return true; } + // TODO: While `express` is not Promise-aware, this should become `async` in + // a major release in order to align the API with other integrations (e.g. + // Hapi) which must be `async`. public applyMiddleware({ app, path, @@ -94,6 +97,23 @@ export class ApolloServer extends ApolloServerBase { }: ServerRegistration) { if (!path) path = '/graphql'; + // Despite the fact that this `applyMiddleware` function is `async` in + // other integrations (e.g. Hapi), currently it is not for Express (@here). + // That should change in a future version, but that would be a breaking + // change right now (see comment above this method's declaration above). + // + // That said, we do need to await the `willStart` lifecycle event which + // can perform work prior to serving a request. Since Express doesn't + // natively support Promises yet, we'll do this via a middleware that + // calls `next` when the `willStart` finishes. We'll kick off the + // `willStart` right away, so hopefully it'll finish before the first + // request comes in, but we won't call `next` on this middleware until it + // does. (And we'll take care to surface any errors via the `.catch`-able.) + const promiseWillStart = this.willStart(); + app.use(path, (_req, _res, next) => { + promiseWillStart.then(() => next()).catch(next); + }); + if (!disableHealthCheck) { // uses same path as engine proxy, but is generally useful. app.use('/.well-known/apollo/server-health', (req, res) => { diff --git a/packages/apollo-server-hapi/src/ApolloServer.ts b/packages/apollo-server-hapi/src/ApolloServer.ts index 77b60ce36c1..5bc2ca8adb7 100644 --- a/packages/apollo-server-hapi/src/ApolloServer.ts +++ b/packages/apollo-server-hapi/src/ApolloServer.ts @@ -53,6 +53,8 @@ export class ApolloServer extends ApolloServerBase { disableHealthCheck, onHealthCheck, }: ServerRegistration) { + await this.willStart(); + if (!path) path = '/graphql'; await app.ext({ diff --git a/packages/apollo-server-koa/src/ApolloServer.ts b/packages/apollo-server-koa/src/ApolloServer.ts index 8f3c7631139..3f92102598b 100644 --- a/packages/apollo-server-koa/src/ApolloServer.ts +++ b/packages/apollo-server-koa/src/ApolloServer.ts @@ -74,6 +74,10 @@ export class ApolloServer extends ApolloServerBase { return true; } + // TODO: While Koa is Promise-aware, this API hasn't been historically, even + // though other integration's (e.g. Hapi) implementations of this method + // are `async`. Therefore, this should become `async` in a major release in + // order to align the API with other integrations. public applyMiddleware({ app, path, @@ -84,6 +88,26 @@ export class ApolloServer extends ApolloServerBase { }: ServerRegistration) { if (!path) path = '/graphql'; + // Despite the fact that this `applyMiddleware` function is `async` in + // other integrations (e.g. Hapi), currently it is not for Koa (@here). + // That should change in a future version, but that would be a breaking + // change right now (see comment above this method's declaration above). + // + // That said, we do need to await the `willStart` lifecycle event which + // can perform work prior to serving a request. While we could do this + // via awaiting in a Koa middleware, well kick off `willStart` right away, + // so hopefully it'll finish before the first request comes in. We won't + // call `next` until it's ready, which will effectively yield until that + // work has finished. Any errors will be surfaced to Koa through its own + // native Promise-catching facilities. + const promiseWillStart = this.willStart(); + app.use( + middlewareFromPath(path, async (_ctx: Koa.Context, next: Function) => { + await promiseWillStart; + return next(); + }), + ); + if (!disableHealthCheck) { // uses same path as engine proxy, but is generally useful. app.use( diff --git a/packages/apollo-server-lambda/src/ApolloServer.ts b/packages/apollo-server-lambda/src/ApolloServer.ts index 396a4d1ad35..62158301167 100644 --- a/packages/apollo-server-lambda/src/ApolloServer.ts +++ b/packages/apollo-server-lambda/src/ApolloServer.ts @@ -44,6 +44,11 @@ export class ApolloServer extends ApolloServerBase { } public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { + // We will kick off the `willStart` event once for the server, and then + // await it before processing any requests by incorporating its `await` into + // the GraphQLServerOptions function which is called before each request. + const promiseWillStart = this.willStart(); + const corsHeaders: lambda.APIGatewayProxyResult['headers'] = {}; if (cors) { @@ -156,11 +161,17 @@ export class ApolloServer extends ApolloServerBase { ); }; - graphqlLambda(this.createGraphQLServerOptions.bind(this))( - event, - context, - callbackFilter, - ); + graphqlLambda(async () => { + // In a world where this `createHandler` was async, we might avoid this + // but since we don't want to introduce a breaking change to this API + // (by switching it to `async`), we'll leverage the + // `GraphQLServerOptions`, which are dynamically built on each request, + // to `await` the `promiseWillStart` which we kicked off at the top of + // this method to ensure that it runs to completion (which is part of + // its contract) prior to processing the request. + await promiseWillStart; + return this.createGraphQLServerOptions(event, context); + })(event, context, callbackFilter); }; } } diff --git a/packages/apollo-server-micro/src/ApolloServer.ts b/packages/apollo-server-micro/src/ApolloServer.ts index 1db04cf3f0f..220f2e090b6 100644 --- a/packages/apollo-server-micro/src/ApolloServer.ts +++ b/packages/apollo-server-micro/src/ApolloServer.ts @@ -30,9 +30,15 @@ export class ApolloServer extends ApolloServerBase { disableHealthCheck, onHealthCheck, }: ServerRegistration = {}) { + // We'll kick off the `willStart` right away, so hopefully it'll finish + // before the first request comes in. + const promiseWillStart = this.willStart(); + return async (req, res) => { this.graphqlPath = path || '/graphql'; + await promiseWillStart; + await this.handleFileUploads(req); (await this.handleHealthCheck({