-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy patherrorHandler.mjs
288 lines (269 loc) · 8.81 KB
/
errorHandler.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
// @ts-check
import createHttpError from "http-errors";
import GraphQLAggregateError from "./GraphQLAggregateError.mjs";
/**
* Creates Koa middleware to handle errors. Use this before other middleware to
* catch all errors for a correctly formatted
* [GraphQL response](https://spec.graphql.org/October2021/#sec-Errors).
*
* Special {@link KoaMiddlewareError Koa middleware error} properties can be
* used to determine how the error appears in the GraphQL response body
* {@linkcode GraphQLResponseBody.errors errors} array and the response HTTP
* status code.
*
* Additional custom Koa middleware can be used to customize the response.
* @see [GraphQL spec for errors](https://spec.graphql.org/October2021/#sec-Errors).
* @see [GraphQL over HTTP spec](https://github.com/graphql/graphql-over-http).
* @see [Koa error handling docs](https://koajs.com/#error-handling).
* @see [`http-errors`](https://npm.im/http-errors), also a Koa dependency.
* @returns Koa middleware.
* @example
* A client error thrown in Koa middleware…
*
* Error constructed manually:
*
* ```js
* const error = new Error("Rate limit exceeded.");
* error.extensions = {
* code: "RATE_LIMIT_EXCEEDED",
* };
* error.status = 429;
* ```
*
* Error constructed using [`http-errors`](https://npm.im/http-errors):
*
* ```js
* import createHttpError from "http-errors";
*
* const error = createHttpError(429, "Rate limit exceeded.", {
* extensions: {
* code: "RATE_LIMIT_EXCEEDED",
* },
* });
* ```
*
* Response has a 429 HTTP status code, with this body:
*
* ```json
* {
* "errors": [
* {
* "message": "Rate limit exceeded.",
* "extensions": {
* "code": "RATE_LIMIT_EXCEEDED"
* }
* }
* ]
* }
* ```
* @example
* A server error thrown in Koa middleware, not exposed to the client…
*
* Error:
*
* ```js
* const error = new Error("Database connection failed.");
* ```
*
* Response has a 500 HTTP status code, with this body:
*
* ```json
* {
* "errors": [
* {
* "message": "Internal Server Error"
* }
* ]
* }
* ```
* @example
* A server error thrown in Koa middleware, exposed to the client…
*
* Error:
*
* ```js
* const error = new Error("Service unavailable due to maintenance.");
* error.status = 503;
* error.expose = true;
* ```
*
* Response has a 503 HTTP status code, with this body:
*
* ```json
* {
* "errors": [
* {
* "message": "Service unavailable due to maintenance."
* }
* ]
* }
* ```
* @example
* An error thrown in a GraphQL resolver, exposed to the client…
*
* Query:
*
* ```graphql
* {
* user(handle: "jaydenseric") {
* name
* email
* }
* }
* ```
*
* Error thrown in the `User.email` resolver:
*
* ```js
* const error = new Error("Unauthorized access to user data.");
* error.expose = true;
* ```
*
* Response has a 200 HTTP status code, with this body:
*
* ```json
* {
* "errors": [
* {
* "message": "Unauthorized access to user data.",
* "locations": [{ "line": 4, "column": 5 }],
* "path": ["user", "email"]
* }
* ],
* "data": {
* "user": {
* "name": "Jayden Seric",
* "email": null
* }
* }
* }
* ```
*/
export default function errorHandler() {
/**
* Koa middleware to handle errors.
* @param {import("koa").ParameterizedContext} ctx Koa context.
* @param {() => Promise<unknown>} next
*/
async function errorHandlerMiddleware(ctx, next) {
try {
// Await all following middleware.
await next();
} catch (error) {
// Create response body if necessary. It may have been created after
// GraphQL execution and contain data.
if (typeof ctx.response.body !== "object" || ctx.response.body == null)
ctx.response.body = {};
const body = /** @type {GraphQLResponseBody} */ (ctx.response.body);
if (
error instanceof GraphQLAggregateError &&
// GraphQL schema validation errors are not exposed.
error.expose
) {
// Error contains GraphQL query validation or execution errors.
body.errors = error.errors.map((graphqlError) => {
const formattedError = graphqlError.toJSON();
return (
// Originally thrown in resolvers (not a GraphQL validation error).
graphqlError.originalError &&
// Not specifically marked to be exposed to the client.
!(
/** @type {KoaMiddlewareError} */ (graphqlError.originalError)
.expose
)
? {
...formattedError,
// Overwrite the message to prevent client exposure. Wording
// is consistent with the http-errors 500 server error
// message.
message: "Internal Server Error",
}
: formattedError
);
});
// For GraphQL query validation errors the status will be 400. For
// GraphQL execution errors the status will be 200; by convention they
// shouldn’t result in a response error HTTP status code.
ctx.response.status = error.status;
} else {
// Error is some other Koa middleware error, possibly GraphQL schema
// validation errors.
// Coerce the error to a HTTP error, in case it’s not one already.
let httpError = createHttpError(
// @ts-ignore Let the library handle an invalid error type.
error
);
// If the error is not to be exposed to the client, use a generic 500
// server error.
if (!httpError.expose) {
httpError = createHttpError(500);
// Assume that an `extensions` property is intended to be exposed to
// the client in the GraphQL response body `errors` array and isn’t
// from something unrelated with a conflicting name.
if (
// Guard against a non enumerable object error, e.g. null.
error instanceof Error &&
typeof (/** @type {KoaMiddlewareError} */ (error).extensions) ===
"object" &&
/** @type {KoaMiddlewareError} */ (error).extensions != null
)
httpError.extensions = /** @type {KoaMiddlewareError} */ (
error
).extensions;
}
body.errors = [
"extensions" in httpError
? {
message: httpError.message,
extensions: httpError.extensions,
}
: {
message: httpError.message,
},
];
ctx.response.status = httpError.status;
}
// Set the content-type.
ctx.response.type = "application/graphql+json";
// Support Koa app error listeners.
ctx.app.emit("error", error, ctx);
}
}
return errorHandlerMiddleware;
}
/**
* An error thrown within Koa middleware following the
* {@linkcode errorHandler} Koa middleware can have these special properties to
* determine how the error appears in the GraphQL response body
* {@linkcode GraphQLResponseBody.errors errors} array and the response HTTP
* status code.
* @see [GraphQL spec for errors](https://spec.graphql.org/October2021/#sec-Errors).
* @see [Koa error handling docs](https://koajs.com/#error-handling).
* @see [`http-errors`](https://npm.im/http-errors), also a Koa dependency.
* @typedef {object} KoaMiddlewareError
* @prop {string} message Error message. If the error
* {@linkcode KoaMiddlewareError.expose expose} property isn’t `true` or the
* {@linkcode KoaMiddlewareError.status status} property >= 500 (for non
* GraphQL resolver errors), the message is replaced with
* `Internal Server Error` in the GraphQL response body
* {@linkcode GraphQLResponseBody.errors errors} array.
* @prop {number} [status] Determines the response HTTP status code. Not usable
* for GraphQL resolver errors as they shouldn’t prevent the GraphQL request
* from having a 200 HTTP status code.
* @prop {boolean} [expose] Should the original error
* {@linkcode KoaMiddlewareError.message message} be exposed to the client.
* @prop {{ [key: string]: unknown }} [extensions] A map of custom error data
* that is exposed to the client in the GraphQL response body
* {@linkcode GraphQLResponseBody.errors errors} array, regardless of the
* error {@linkcode KoaMiddlewareError.expose expose} or
* {@linkcode KoaMiddlewareError.status status} properties.
*/
/**
* A GraphQL response body.
* @see [GraphQL over HTTP spec](https://github.com/graphql/graphql-over-http).
* @typedef {object} GraphQLResponseBody
* @prop {Array<import("graphql").GraphQLFormattedError>} [errors] Errors.
* @prop {{ [key: string]: unknown } | null} [data] Data.
* @prop {{ [key: string]: unknown }} [extensions] Custom extensions to the
* GraphQL over HTTP protocol.
*/