Skip to content

Commit

Permalink
Merge branch 'refine-http-status-code-behaviour' of https://github.co…
Browse files Browse the repository at this point in the history
  • Loading branch information
mcollina committed Dec 20, 2021
2 parents feeba8b + d34e20d commit fa7c4ae
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 14 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -37,7 +37,11 @@ Features:
- [Integrations](docs/integrations/)
- [Related Plugins](docs/plugins.md)
- [Protocol Extensions](/docs/protocol-extension.md)
<<<<<<< HEAD
- [Faq](/docs/faq.md)
=======
- [HTTP](/docs/http.md)
>>>>>>> d34e20da58eb800cbd5f13465ea8a2ccd1f9b10b
- [Acknowledgements](#acknowledgements)
- [License](#license)

Expand Down
21 changes: 9 additions & 12 deletions docs/api/options.md
Expand Up @@ -537,24 +537,21 @@ app.listen(3000)
To control the status code for the response, the third optional parameter can be used.

```js
throw new mercurius.ErrorWithProps('Invalid User ID', {moreErrorInfo})
// using the defaultErrorFormatter, the response statusCode will be 500 when there is a single error

throw new mercurius.ErrorWithProps('Invalid User ID', {moreErrorInfo})
// using de defaultErrorFormatter the response statusCode will be 500

throw new mercurius.ErrorWithProps('Invalid User ID', {moreErrorInfo}, 200)
// using de defaultErrorFormatter the response statusCode will be 200

const error = new mercurius.ErrorWithProps('Invalid User ID', {moreErrorInfo}, 500)
error.data = {foo: 'bar'}
throw error
// using de defaultErrorFormatter the response status code will be always 200 because error.data is defined

throw new mercurius.ErrorWithProps('Invalid User ID', {moreErrorInfo}, 200)
// using the defaultErrorFormatter, the response statusCode will be 200 when there is a single error

const error = new mercurius.ErrorWithProps('Invalid User ID', {moreErrorInfo}, 500)
error.data = {foo: 'bar'}
throw error
// using the defaultErrorFormatter, the response status code will be always 200 because error.data is defined
```

### Error formatter

Allows the status code of the response to be set, and a GraphQL response for the error to be defined.
Allows the status code of the response to be set, and a GraphQL response for the error to be defined. You find out how to do this [here](../http.md#custom-behaviour).

By default uses the `defaultErrorFormatter`, but it can be overridden in the [mercurius options](/docs/api/options.md#plugin-options) changing the errorFormatter parameter.

Expand Down
137 changes: 137 additions & 0 deletions docs/http.md
@@ -0,0 +1,137 @@
# mercurius

- [HTTP Status Codes](#http-status-codes)
- [Default behaviour](#default-behaviour)
- [Response with data](#response-with-data)
- [Invalid input document](#invalid-input-document)
- [Multiple errors](#multiple-errors)
- [Single error with `statusCode` property](#single-error-with-statuscode-property)
- [Single error with no `statusCode` property](#single-error-with-no-statuscode-property)
- [Custom behaviour](#custom-behaviour)
- [`200 OK` on all requests](#200-ok-on-all-requests)

Mercurius exhibits the following behaviour when serving GraphQL over HTTP.

## HTTP Status Codes

### Default behaviour

Mercurius has the following default behaviour for HTTP Status Codes.

#### Response with data

When a GraphQL response contains `data` that is defined, the HTTP Status Code is `200 OK`.

- **HTTP Status Code**: `200 OK`
- **Data**: `!== null`
- **Errors**: `N/A`

#### Invalid input document

When a GraphQL input document is invalid and fails GraphQL validation, the HTTP Status Code is `400 Bad Request`.

- **HTTP Status Code**: `400 Bad Request`
- **Data**: `null`
- **Errors**: `MER_ERR_GQL_VALIDATION`

#### Multiple errors

When a GraphQL response contains multiple errors and no data, the HTTP Status Code is `400 Bad Request`.

- **HTTP Status Code**: `400 Bad Request`
- **Data**: `null`
- **Errors**: `Multiple`

#### Single error with `statusCode` property

When a GraphQL response contains a single error with the `statusCode` property set and no data, the HTTP Status Code is set to this value. See [ErrorWithProps](./api/options.md#errorwithprops) for more details.

- **HTTP Status Code**: `Error statusCode`
- **Data**: `null`
- **Errors**: `Single`

#### Single error with no `statusCode` property

When a GraphQL response contains a single error with no `statusCode` property set and no data, the HTTP Status Code is `500 Internal Server Error`.

- **HTTP Status Code**: `500 Internal Server Error`
- **Data**: `null`
- **Errors**: `Single with no .statusCode property`

### Custom behaviour

If you wish to customise the default HTTP Status Code behaviour, one can do this using the [`errorFormatter`](./api/options.md#plugin-options) option.

#### `200 OK` on all requests

Enable `200 OK` on all requests as follows:

```js
'use strict'

const Fastify = require('fastify')
const mercurius = require('..')

const app = Fastify()

const schema = `
type Query {
add(x: Int, y: Int): Int
}
`

const resolvers = {
Query: {
add: async (_, obj) => {
const { x, y } = obj
return x + y
}
}
}

/**
* Define error formatter so we always return 200 OK
*/
function errorFormatter (err, ctx) {
const response = mercurius.defaultErrorFormatter(err, ctx)
response.statusCode = 200
return response
}

app.register(mercurius, {
schema,
resolvers,
errorFormatter
})

app.listen(3000)
```

With an invalid request:

```graphql
{
add(wrong: 1 x: 2)
}
```

The response is:

HTTP Status Code: `200 OK`

```json
{
"data": null,
"errors": [
{
"message": "Unknown argument \"wrong\" on field \"Query.add\".",
"locations": [
{
"line": 2,
"column": 7
}
]
}
]
}
```
5 changes: 5 additions & 0 deletions docsify/sidebar.md
Expand Up @@ -19,6 +19,7 @@
* [Batched Queries](/docs/batched-queries)
* [Persisted Queries](/docs/persisted-queries)
* [TypeScript Usage](/docs/typescript)
* [HTTP](/docs/http)
* [Integrations](/docs/integrations/)
* [nexus](/docs/integrations/nexus)
* [TypeGraphQL](/docs/integrations/type-graphql)
Expand All @@ -27,7 +28,11 @@
* [Tracing - OpenTelemetry](/docs/integrations/open-telemetry)
* [Related Plugins](/docs/plugins)
* [mercurius-auth](/docs/plugins#mercurius-auth)
* [mercurius-cache](/docs/plugins#mercurius-cache)
* [mercurius-validation](/docs/plugins#mercurius-validation)
* [mercurius-upload](/docs/plugins#mercurius-upload)
* [altair-fastify-plugin](/docs/plugins#altair-fastify-plugin)
* [mercurius-apollo-registry](/docs/plugins#mercurius-apollo-registry)
* [mercurius-apollo-tracing](/docs/plugins#mercurius-apollo-tracing)
* [Protocol Extensions](/docs/protocol-extension)
* [Faq](/docs/faq)
38 changes: 38 additions & 0 deletions examples/custom-http-behaviour.js
@@ -0,0 +1,38 @@
'use strict'

const Fastify = require('fastify')
const mercurius = require('..')

const app = Fastify()

const schema = `
type Query {
add(x: Int, y: Int): Int
}
`

const resolvers = {
Query: {
add: async (_, obj) => {
const { x, y } = obj
return x + y
}
}
}

/**
* Define error formatter so we always return 200 OK
*/
function errorFormatter (err, ctx) {
const response = mercurius.defaultErrorFormatter(err, ctx)
response.statusCode = 200
return response
}

app.register(mercurius, {
schema,
resolvers,
errorFormatter
})

app.listen(3000)
19 changes: 18 additions & 1 deletion lib/errors.js
Expand Up @@ -47,6 +47,7 @@ function defaultErrorFormatter (err, ctx) {
// There is always app if there is a context
const log = ctx.reply ? ctx.reply.log : ctx.app.log

let statusCode = err.data ? 200 : (err.statusCode || 500)
if (err.errors) {
errors = err.errors.map((error, idx) => {
log.info({ err: error }, error.message)
Expand All @@ -59,12 +60,28 @@ function defaultErrorFormatter (err, ctx) {
// as the result of the outer map could potentially contain arrays with federated errors
// the result needs to be flattened
}).reduce((acc, val) => acc.concat(val), [])

// Override status code when there is no data or statusCode present
if (!err.data && typeof err.statusCode === 'undefined' && err.errors.length > 0) {
if (errors.length === 1) {
// If single error defined, use status code if present
if (typeof err.errors[0].originalError !== 'undefined' && typeof err.errors[0].originalError.statusCode === 'number') {
statusCode = err.errors[0].originalError.statusCode
// Otherwise, use 500
} else {
statusCode = 500
}
} else {
// Otherwise, if multiple errors are defined, set status code to 400
statusCode = 400
}
}
} else {
log.info({ err }, err.message)
}

return {
statusCode: err.data ? 200 : /* istanbul ignore next */ (err.statusCode || 500),
statusCode,
response: {
data: err.data || null,
errors
Expand Down

0 comments on commit fa7c4ae

Please sign in to comment.