Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async server.start() function #4981

Merged
merged 10 commits into from Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,8 @@ The version headers in this history reflect the versions of Apollo Server itself

> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. With few exceptions, the format of the entry should follow convention (i.e., prefix with package name, use markdown `backtick formatting` for package names and code, suffix with a link to the change-set à la `[PR #YYY](https://link/pull/YYY)`, etc.). When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section.

- Improve startup error handling by ensuring that your server has loaded its schema and executed its `serverWillStart` handlers successfully before starting an HTTP server. If you're using the `apollo-server` package, no code changes are necessary. If you're using an integration such as `apollo-server-express` that is not a "serverless framework", you can insert [`await server.start()`](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#start) between `server = new ApolloServer()` and `server.applyMiddleware`. (If you don't call `server.start()` yourself, your server will still work, but the previous behavior of starting a web server that may fail to load its schema still applies.) The serverless framework integrations (Lambda, Azure Functions, and Cloud Functions) do not support this functionality. While the protected method `willStart` still exists for backwards compatibility, you should replace calls to it with `start` or the new protected method `ensureStarting`. [PR #4981](https://github.com/apollographql/apollo-server/pull/4981)

## v2.21.2

- `apollo-server-core`: The `SIGINT` and `SIGTERM` signal handlers installed by default (when not disabled by `stopOnTerminationSignals: false`) now stay active (preventing process termination) while the server shuts down, instead of letting a second signal terminate the process. The handlers still re-signal the process after `this.stop()` concludes. Also, if `this.stop()` throws, the signal handlers will now log and exit 1 instead of throwing an uncaught exception. [Issue #4931](https://github.com/apollographql/apollo-server/issues/4931)
Expand Down
33 changes: 31 additions & 2 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 41 additions & 11 deletions docs/source/api/apollo-server.md
Expand Up @@ -617,7 +617,7 @@ A lifecycle hook that's called whenever a subscription connection is terminated

#### `listen`

> This method is provided only by the `apollo-server` package. If you're integrating with Node.js middleware via a different package (such as `apollo-server-express`), instead see [`applyMiddleware`](#applymiddleware).
> This method is provided only by the `apollo-server` package. If you're integrating with Node.js middleware via a different package (such as `apollo-server-express`), instead see both [`start`](#start) and [`applyMiddleware`](#applymiddleware).

Instructs Apollo Server to begin listening for incoming requests:

Expand Down Expand Up @@ -697,11 +697,35 @@ The full URL of the server's subscriptions endpoint.
</tbody>
</table>

#### `start`

The async `start` method instructs Apollo Server to prepare to handle incoming operations.

> Call `start` **only** if you are using a [middleware integration](../integrations/middleware/) for a non-"serverless" environment (e.g., `apollo-server-express`).
>
> * If you're using the core `apollo-server` library, call [`listen`](#listen) instead.
> * If you're using a "serverless" middleware integration (such as `apollo-server-lambda`), this method isn't necessary because the integration doesn't distinguish between starting the server and serving a request.

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.

##### Triggered actions

The `start` method triggers the following actions:

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.
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.

##### Backward compatibility

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).

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.

#### `applyMiddleware`

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

You call this method instead of [`listen`](#listen) if you're using an `apollo-server-{integration}` package.
You call this method instead of [`listen`](#listen) if you're using a [middleware integration](../integrations/middleware/), such as `apollo-server-express`. You should call [`await server.start()`](#start) _before_ calling this method.

Takes an `options` object as a parameter. Supported fields of this object are described below.

Expand All @@ -712,18 +736,24 @@ const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const { typeDefs, resolvers } = require('./schema');

const server = new ApolloServer({
typeDefs,
resolvers,
});
async function startApolloServer() {
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();

const app = express();
const app = express();

// Additional middleware can be mounted at this point to run before Apollo.
app.use('*', jwtCheck, requireAuth, checkScope);
// Additional middleware can be mounted at this point to run before Apollo.
app.use('*', jwtCheck, requireAuth, checkScope);

// Mount Apollo middleware here.
server.applyMiddleware({ app, path: '/specialUrl' });
// Mount Apollo middleware here.
server.applyMiddleware({ app, path: '/specialUrl' });
await new Promise(resolve => app.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
return { server, app };
}
```

##### Options
Expand Down
31 changes: 16 additions & 15 deletions docs/source/data/subscriptions.mdx
Expand Up @@ -411,28 +411,29 @@ In case of an authentication error, the Promise will be rejected, which prevents

You can use subscriptions with any of Apollo Server's supported [middleware integrations](../integrations/middleware/). To do so, you call `installSubscriptionHandlers` on your `ApolloServer` instance.


This example enables subscriptions for an Express server that uses `apollo-server-express`:

```js
const http = require('http');
const { ApolloServer } = require('apollo-server-express');
const express = require('express');

const PORT = 4000;
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });

server.applyMiddleware({app})

const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer); // highlight-line

// Make sure to call listen on httpServer, NOT on app.
httpServer.listen(PORT, () => {
console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`)
console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`)
})
async function startApolloServer() {
const PORT = 4000;
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
server.applyMiddleware({app})

const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer); // highlight-line

// Make sure to call listen on httpServer, NOT on app.
await new Promise(resolve => httpServer.listen(PORT, resolve));
console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`);
return { server, app, httpServer };
}
```

## Production `PubSub` libraries
Expand Down
3 changes: 1 addition & 2 deletions docs/source/deployment/azure-functions.md
Expand Up @@ -64,7 +64,7 @@ npm init -y
npm install apollo-server-azure-functions graphql
```

Copy the code below and paste at you **index.js** file.
Copy the code below and paste in your **index.js** file.

```javascript
const { ApolloServer, gql } = require('apollo-server-azure-functions');
Expand All @@ -84,7 +84,6 @@ const resolvers = {
};

const server = new ApolloServer({ typeDefs, resolvers });

exports.graphqlHandler = server.createHandler();
```

Expand Down
2 changes: 1 addition & 1 deletion docs/source/deployment/netlify.md
Expand Up @@ -90,7 +90,7 @@ exports.handler = server.createHandler();

Now, make sure you've run `NODE_ENV=development npm run start:lambda`, and navigate to `localhost:9000/graphql` in your browser. You should see GraphQL Playground, where you can run queries against your API!

*Note - The GraphQL Playground will only run if your `NODE_ENV` is set to `development`. If you don't pass this, or your `NODE_ENV` is set to `production`, you will not see the GraphQL Playground.*
*Note - The GraphQL Playground will only run if your `NODE_ENV` is set to `development`. If you don't pass this, or your `NODE_ENV` is set to `production`, you will not see the GraphQL Playground.*

![Local GraphQL Server](../images/graphql.png)

Expand Down
37 changes: 20 additions & 17 deletions docs/source/integrations/middleware.md
Expand Up @@ -36,23 +36,26 @@ const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const { typeDefs, resolvers } = require('./schema');

const app = express();
const server = new ApolloServer({
typeDefs,
resolvers,
});

server.applyMiddleware({ app });

app.use((req, res) => {
res.status(200);
res.send('Hello!');
res.end();
});

app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
)
async function startApolloServer() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example could benefit from calling startApolloServer, otherwise it may be indiscernible to someone copying and pasting this example as to why the server hasn't started.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, the problem is... how are you supposed to call it? The whole point is I don't want to encourage people to call it in a context where errors will turn into unhandledRejection. So I could show it being called from top-level await... but then I might as well just revert back to not having a function. Or I could show it being called with .catch() but that seems like it would get distracting (and it's unclear that a particular bit of .catch() from our docs is actually the best way to integrate it into your system).

While Node does now support top-level await (as of v14.8.0), it requires you to be in a special "module" mode where your file ends with .mjs, or set a flag in package.json. And it's different in TypeScript... here you have to be in module module (which should be the case for any of these examples because of the use of import) but you also have to set the target and/or module tsconfig option appropriately...

It feels like this is a big bundle of worms, compared to "here's an async function and it's your job to know how to use it". Since this mostly affects "advanced" uses (I'm not editing any calls to apollo-server's listen()) I'm a little less worried?

I mean, the fact that our apollo-server examples tell you to call listen() with only .then and not .catch is another issue, and perhaps we should go through and change those all to use top-level await with explicit guidance to use .mjs files, but that's a project for another day...

const app = express();
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();

server.applyMiddleware({ app });

app.use((req, res) => {
res.status(200);
res.send('Hello!');
res.end();
});

await new Promise(resolve => app.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
return { server, app };
}
```

The parameter you provide to `applyMiddleware` is your middleware's top-level representation of your application. In Express applications, this variable is commonly named `app`.
Expand Down
87 changes: 45 additions & 42 deletions docs/source/security/terminating-ssl.md
Expand Up @@ -12,48 +12,51 @@ library](/integrations/middleware/).
Here's an example that uses HTTPS in production and HTTP in development:

```javascript
import express from 'express'
import { ApolloServer } from 'apollo-server-express'
import typeDefs from './graphql/schema'
import resolvers from './graphql/resolvers'
import fs from 'fs'
import https from 'https'
import http from 'http'

const configurations = {
// Note: You may need sudo to run on port 443
production: { ssl: true, port: 443, hostname: 'example.com' },
development: { ssl: false, port: 4000, hostname: 'localhost' }
}

const environment = process.env.NODE_ENV || 'production'
const config = configurations[environment]

const apollo = new ApolloServer({ typeDefs, resolvers })

const app = express()
apollo.applyMiddleware({ app })

// Create the HTTPS or HTTP server, per configuration
var server
if (config.ssl) {
// Assumes certificates are in a .ssl folder off of the package root. Make sure
// these files are secured.
server = https.createServer(
{
key: fs.readFileSync(`./ssl/${environment}/server.key`),
cert: fs.readFileSync(`./ssl/${environment}/server.crt`)
},
app
)
} else {
server = http.createServer(app)
}

server.listen({ port: config.port }, () =>
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import typeDefs from './graphql/schema';
import resolvers from './graphql/resolvers';
import fs from 'fs';
import https from 'https';
import http from 'http';

async function startApolloServer() {
const configurations = {
// Note: You may need sudo to run on port 443
production: { ssl: true, port: 443, hostname: 'example.com' },
development: { ssl: false, port: 4000, hostname: 'localhost' },
};

const environment = process.env.NODE_ENV || 'production';
const config = configurations[environment];

const server = new ApolloServer({ typeDefs, resolvers });
await server.start();

const app = express();
server.applyMiddleware({ app });

// Create the HTTPS or HTTP server, per configuration
let server;
if (config.ssl) {
// Assumes certificates are in a .ssl folder off of the package root. Make sure
// these files are secured.
server = https.createServer(
{
key: fs.readFileSync(`./ssl/${environment}/server.key`),
cert: fs.readFileSync(`./ssl/${environment}/server.crt`)
},
app,
);
} else {
server = http.createServer(app);
}

await new Promise(resolve => server.listen({ port: config.port }, resolve));
console.log(
'🚀 Server ready at',
`http${config.ssl ? 's' : ''}://${config.hostname}:${config.port}${apollo.graphqlPath}`
)
)
`http${config.ssl ? 's' : ''}://${config.hostname}:${config.port}${server.graphqlPath}`
);
return { server, app };
}
```
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -57,6 +57,7 @@
},
"devDependencies": {
"@apollographql/graphql-upload-8-fork": "8.1.3",
"@josephg/resolvable": "^1.0.0",
"@types/async-retry": "1.4.2",
"@types/aws-lambda": "8.10.66",
"@types/body-parser": "1.19.0",
Expand Down