Skip to content

Commit

Permalink
New error extension http for setting status/headers in context, res…
Browse files Browse the repository at this point in the history
…olvers (#6857)

You can now add a `Partial<HTTPGraphqlHead>` as an `http` error
extension, which provides an easier way of setting HTTP response headers
or status codes. These are honored when thrown from resolvers and
`context` functions (and are also used internally to implement bad
request errors etc).

Multiple resolver errors can have `http` extensions to set status code
and distinct headers separately; we do not commit to the semantics of
what happens if multiple errors set the status code or set the same
header.

Now that you have more control over `context` error handling, simplify
the default behavior: if the error thrown from `context` is not a
`GraphQLError` with an `http` extension with a `status`, the default
status code is 500 rather than sometimes being 400 based on
`extensions.code`.

Only prepend `Context creation failed: ` to the error message if the
error is not already a GraphQLError. This still keeps the helpful
message for developers while allowing you to remove it if you don't want
it to be visible in your app.

Fixes #6140. Fixes #5636. Fixes #6840.

Co-authored-by: Rose M Koron <32436232+rkoron007@users.noreply.github.com>
  • Loading branch information
glasser and rkoron007 committed Aug 29, 2022
1 parent f257ee6 commit 15b1cb2
Show file tree
Hide file tree
Showing 13 changed files with 577 additions and 195 deletions.
6 changes: 6 additions & 0 deletions .changeset/witty-squids-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@apollo/server-integration-testsuite": patch
"@apollo/server": patch
---

Errors thrown in resolvers and context functions can use `extensions.http` to affect the response status code and headers. The default behavior when a context function throws is now to always use status code 500 rather than comparing `extensions.code` to `INTERNAL_SERVER_ERROR`.
54 changes: 50 additions & 4 deletions docs/source/data/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -604,21 +604,67 @@ In this case, the error above is reported to Apollo Studio as:
The REDACTED doesn't have sufficient privileges.
```

## Returning HTTP status codes
## Setting HTTP status code and headers

GraphQL, by design, does not use the same conventions from REST to communicate via HTTP verbs and status codes. Client information should be contained in the schema or as part of the standard response `errors` field. We recommend using the included [Error Codes](#built-in-error-codes) or [Custom Errors](#custom-errors) for error consistency rather than directly modifying the HTTP response.

You can set custom fields on your HTTP response by using a [plugin](/apollo-server/integrations/plugins). Be aware that GraphQL client libraries may not treat all response status codes the same, and so it will be up to your team to decide what patterns to use.
Apollo Server uses different HTTP status codes in various situations:
- If Apollo Server hasn't correctly started up or is in the process of shutting down, it responds with a 500 status code.
- The former can happen if you use a serverless integration and it sends requests to an Apollo Server instance that had an error on startup. The latter happens if you aren't properly [draining your server](/apollo-server/api/plugin/drain-http-server/#using-the-plugin).
- If Apollo Server can't parse the request into a legal GraphQL document and validate it against your schema, it responds with a 400 status code. This can also happen with other request problems, such as if a client attempts to send a batched HTTP request when `allowBatchedHttpRequests` isn't enabled or if CSRF prevention blocks a request.
- If a request uses an invalid HTTP method (`GET` with a mutation, or any HTTP method other than `GET` or `POST`), then Apollo Server responds with a 405 status code.
- If your `context` function throws, Apollo Server responds with a 500 status code.
- If there is an unexpected error during the processing of the request (either a bug in Apollo Server or a plugin hook throws), Apollo Server responds with a 500 status code.
- Otherwise, Apollo Server returns a 200 status code. This is essentially the case where the server can execute the GraphQL operation, and execution completes successfully (though this can still include resolver-specific errors).

As an example, here is how you could set a custom response header and status code based on a GraphQL error:
There are three ways to change an HTTP status code or set custom response headers, you can: throw an error in a resolver, throw an error in your `context` function, or write a [plugin](/apollo-server/integrations/plugins).

While Apollo Server does enable you to set HTTP status codes based on errors thrown by resolvers, best practices for GraphQL over HTTP encourage sending 200 whenever an operation executes. So, we don't recommend using this mechanism in resolvers, just in the `context` function or in a plugin hooking into an early stage of the request pipeline.

Be aware that GraphQL client libraries might not treat all response status codes the same, so it will be up to your team to decide which patterns to use.

To change the HTTP status code and response headers based on an error thrown in either a resolver or `context` function, throw a `GraphQLError` with an `http` extension, like so:

<MultiCodeBlock>

```ts
import { GraphQLError } from 'graphql';

const resolvers = {
Query: {
someField() {
throw new GraphQLError('the error message', {
extensions: {
code: 'SOMETHING_BAD_HAPPENED',
http: {
status: 404,
headers: new Map([
['some-header', 'it was bad'],
['another-header', 'seriously'],
]),
},
},
});
}
}
}
```

</MultiCodeBlock>

You don't need to include `status` unless you want to override the default status code (200 for a resolver or 500 for a `context` function). The optional `headers` field should provide a `Map` with lowercase header names.

If your setup includes multiple resolvers which throw errors that set status codes or set the same header, Apollo Server might resolve this conflict in an arbitrary way (which could change in future versions). Instead, we recommend writing a plugin (as shown below).

You can also set the HTTP status code and headers from a plugin. As an example, here is how you could set a custom response header and status code based on a GraphQL error:

```ts
const setHttpPlugin = {
async requestDidStart() {
return {
async willSendResponse({ response }) {
response.http.headers.set('custom-header', 'hello');
if (response?.result?.errors?.[0]?.message === 'teapot') {
if (response?.result?.errors?.[0]?.extensions?.code === 'TEAPOT') {
response.http.status = 418;
}
},
Expand Down
27 changes: 27 additions & 0 deletions docs/source/data/resolvers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,33 @@ context: async () => ({
}
```

#### Throwing errors in `context`

By default, if your `context` function throws an error, Apollo Server returns that error to the user in a JSON response with an HTTP status code of 500. If the thrown error is not a `GraphQLError`, the error's message will be prepended with `"Context creation failed: "`.
You can control the HTTP status code of an error by throwing a [`GraphQLError` with an `http` extension](./errors/#setting-http-status-code-and-headers). For example:
```ts
context: async ({ req }) => {
const user = await getUserFromReq(req);
if (!user) {
throw new GraphQLError('User is not authenticated', {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
}
});
}
// If this throws a non-GraphQLError, it will be rendered with
// `code: "INTERNAL_SERVER_ERROR"` and HTTP status code 500, and
// with a message starting with "Context creation failed: ".
const db = await getDatabaseConnection();

return { user, db };
},
````

## Return values

A resolver function's return value is treated differently by Apollo Server depending on its type:
Expand Down
12 changes: 8 additions & 4 deletions docs/source/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ class ContextValue {
public dataSources: {
moviesAPI: MoviesAPI;
};

constructor({ req, server }: { req: IncomingMessage; server: ApolloServer<ContextValue> }) {
this.token = getTokenFromRequest(req);
const { cache } = server;
Expand Down Expand Up @@ -1302,9 +1302,9 @@ Apollo Server 3.5.0 and newer included a TypeScript `declare module` declaration



### HTTP error handling changes
### Improvements to error handling outside of resolvers

Apollo Server 3 returns specific errors relating to GraphQL operations over HTTP/JSON as `text/plain` error messages.
Apollo Server 3 returns some errors relating to GraphQL operations over HTTP/JSON as `text/plain` error messages.

Apollo Server 4 now returns all non-landing-page-related responses as `application/json` JSON responses. This means all single-error responses render like any other GraphQL error:

Expand All @@ -1318,8 +1318,12 @@ Additionally, the [`formatError` hook](/apollo-server/data/errors/#for-client-re

Apollo Server 4 also introduces new plugin hooks `startupDidFail`, `contextCreationDidFail`, `invalidRequestWasReceived`, and `unexpectedErrorProcessingRequest`, enabling plugins to observe errors in new settings.

In Apollo Server 3, if your `context` function throws, then the string `"Context creation failed: "` is *always* prepended to its message, and the error is rendered with HTTP status code 500 (if the error is a GraphQLError with `extensions.code` equal to `INTERNAL_SERVER_ERROR`) or 400. You cannot select a different HTTP status code or control HTTP response headers.

In Apollo Server 4, if either the `resolveOperation` or `execute` function throws an error, that error is rendered with the HTTP status code 500 (rather than 400). Note that the `execute` function commonly returns a non-empty list of errors rather than throwing an explicit error.

In Apollo Server 4, if your `context` function throws, the string `"Context creation failed: "` is only prepended to the message if the thrown error was not a `GraphQLError`. There is no special-casing of `extensions.code`; instead, you can use [`extensions.http`](./data/errors/#setting-http-status-code-and-headers) to set the HTTP status code or headers. If this extension is not provided, the status code defaults to 500 (not 400).


### Warning for servers without draining

Expand Down Expand Up @@ -1618,7 +1622,7 @@ new ApolloServer({
## Renamed packages

The following packages have been renamed in Apollo Server 4:
- `apollo-datasource-rest` is now [`@apollo/datasource-rest`](https://www.npmjs.com/package/@apollo/datasource-rest).
- `apollo-datasource-rest` is now [`@apollo/datasource-rest`](https://www.npmjs.com/package/@apollo/datasource-rest).
- `apollo-server-plugin-response-cache` is now [`@apollo/server-plugin-response-cache`](https://www.npmjs.com/package/@apollo/server-plugin-response-cache).
- `apollo-server-plugin-operation-registry` is now [`@apollo/server-plugin-operation-registry`](https://www.npmjs.com/package/@apollo/server-plugin-operation-registry).
- `apollo-reporting-protobuf` (an internal implementation detail for the usage reporting plugin) is now [`@apollo/usage-reporting-protobuf`](https://www.npmjs.com/package/@apollo/usage-reporting-protobuf).
Expand Down
Loading

0 comments on commit 15b1cb2

Please sign in to comment.