graphql-http 1.17.1 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +17 -1
  2. package/lib/audits/common.d.mts +1 -1
  3. package/lib/audits/common.d.ts +1 -1
  4. package/lib/audits/render.js +15 -0
  5. package/lib/audits/render.mjs +15 -0
  6. package/lib/audits/server.js +42 -172
  7. package/lib/audits/server.mjs +42 -172
  8. package/lib/audits/utils.js +6 -3
  9. package/lib/audits/utils.mjs +6 -3
  10. package/lib/handler.d.mts +20 -3
  11. package/lib/handler.d.ts +20 -3
  12. package/lib/handler.js +36 -14
  13. package/lib/handler.mjs +37 -15
  14. package/lib/use/express.js +1 -18
  15. package/lib/use/express.mjs +1 -18
  16. package/lib/use/fastify.js +1 -19
  17. package/lib/use/fastify.mjs +1 -19
  18. package/lib/use/fetch.d.mts +1 -1
  19. package/lib/use/fetch.d.ts +1 -1
  20. package/lib/use/fetch.js +2 -22
  21. package/lib/use/fetch.mjs +2 -22
  22. package/lib/use/http.d.mts +1 -1
  23. package/lib/use/http.d.ts +1 -1
  24. package/lib/use/http.js +2 -19
  25. package/lib/use/http.mjs +2 -19
  26. package/lib/use/http2.d.mts +1 -1
  27. package/lib/use/http2.d.ts +1 -1
  28. package/lib/use/http2.js +2 -19
  29. package/lib/use/http2.mjs +2 -19
  30. package/lib/use/koa.js +0 -14
  31. package/lib/use/koa.mjs +0 -14
  32. package/lib/use/node.d.mts +1 -1
  33. package/lib/use/node.d.ts +1 -1
  34. package/lib/use/node.js +1 -1
  35. package/lib/use/node.mjs +1 -1
  36. package/lib/use/uWebSockets.d.mts +36 -0
  37. package/lib/use/uWebSockets.d.ts +36 -0
  38. package/lib/use/uWebSockets.js +84 -0
  39. package/lib/use/uWebSockets.mjs +80 -0
  40. package/lib/utils.d.mts +2 -0
  41. package/lib/utils.d.ts +2 -0
  42. package/lib/utils.js +15 -1
  43. package/lib/utils.mjs +13 -0
  44. package/package.json +30 -25
  45. package/umd/graphql-http-audits.js +63 -175
  46. package/umd/graphql-http-audits.min.js +1 -1
  47. package/umd/graphql-http-audits.min.js.gz +0 -0
@@ -54,9 +54,13 @@ export function serverAudits(opts) {
54
54
  ressert(res).header('content-type').toContain('application/json');
55
55
  }),
56
56
  audit('80D8', 'SHOULD assume application/json content-type when accept is missing', async () => {
57
- const url = new URL(await getUrl(opts.url));
58
- url.searchParams.set('query', '{ __typename }');
59
- const res = await fetchFn(url.toString());
57
+ const res = await fetchFn(await getUrl(opts.url), {
58
+ method: 'POST',
59
+ headers: {
60
+ 'content-type': 'application/json',
61
+ },
62
+ body: JSON.stringify({ query: '{ __typename }' }),
63
+ });
60
64
  ressert(res).status.toBe(200);
61
65
  ressert(res).header('content-type').toContain('application/json');
62
66
  }),
@@ -127,7 +131,7 @@ export function serverAudits(opts) {
127
131
  ressert(res).status.toBeBetween(400, 499);
128
132
  }),
129
133
  // Request POST
130
- audit('9ABE', 'SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
134
+ audit('9ABE', 'MAY respond with 4xx status code if content-type is not supplied on POST requests', async () => {
131
135
  const res = await fetchFn(await getUrl(opts.url), {
132
136
  method: 'POST',
133
137
  });
@@ -141,23 +145,15 @@ export function serverAudits(opts) {
141
145
  });
142
146
  ressert(res).status.toBe(200);
143
147
  }),
144
- audit('7267', 'MUST require a request body on POST', async () => {
145
- var _a;
148
+ audit('A5BF', 'MAY use 400 status code when request body is missing on POST', async () => {
146
149
  const res = await fetchFn(await getUrl(opts.url), {
147
150
  method: 'POST',
148
151
  headers: { 'content-type': 'application/json' },
149
152
  });
150
- if ((_a = res.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.includes('application/json')) {
151
- await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
152
- }
153
- else {
154
- ressert(res).status.toBe(400);
155
- }
153
+ ressert(res).status.toBe(400);
156
154
  }),
157
155
  // Request Parameters
158
- audit(
159
- // TODO: convert to MUST after watershed
160
- '6610', 'SHOULD use 400 status code on missing {query} parameter when accepting application/graphql-response+json', async () => {
156
+ audit('423L', 'MAY use 400 status code on missing {query} parameter', async () => {
161
157
  const res = await fetchFn(await getUrl(opts.url), {
162
158
  method: 'POST',
163
159
  headers: {
@@ -168,26 +164,11 @@ export function serverAudits(opts) {
168
164
  });
169
165
  ressert(res).status.toBe(400);
170
166
  }),
171
- audit('3715', 'SHOULD use 200 status code with errors field on missing {query} parameter when accepting application/json', async () => {
167
+ ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`LKJ${index}`, `MAY use 400 status code on ${extendedTypeof(invalid)} {query} parameter`, async () => {
172
168
  const res = await fetchFn(await getUrl(opts.url), {
173
169
  method: 'POST',
174
170
  headers: {
175
171
  'content-type': 'application/json',
176
- accept: 'application/json',
177
- },
178
- body: JSON.stringify({ notquery: '{ __typename }' }),
179
- });
180
- ressert(res).status.toBe(200);
181
- await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
182
- }),
183
- ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`4F5${index}`,
184
- // TODO: convert to MUST after watershed
185
- `SHOULD use 400 status code on ${extendedTypeof(invalid)} {query} parameter when accepting application/graphql-response+json`, async () => {
186
- const res = await fetchFn(await getUrl(opts.url), {
187
- method: 'POST',
188
- headers: {
189
- 'content-type': 'application/json',
190
- accept: 'application/graphql-response+json',
191
172
  },
192
173
  body: JSON.stringify({
193
174
  query: invalid,
@@ -195,20 +176,6 @@ export function serverAudits(opts) {
195
176
  });
196
177
  ressert(res).status.toBe(400);
197
178
  })),
198
- ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`9FE${index}`, `SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {query} parameter when accepting application/json`, async () => {
199
- const res = await fetchFn(await getUrl(opts.url), {
200
- method: 'POST',
201
- headers: {
202
- 'content-type': 'application/json',
203
- accept: 'application/json',
204
- },
205
- body: JSON.stringify({
206
- query: invalid,
207
- }),
208
- });
209
- ressert(res).status.toBe(200);
210
- await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
211
- })),
212
179
  audit(
213
180
  // TODO: convert to MUST after watershed
214
181
  '34A2', 'SHOULD allow string {query} parameter when accepting application/graphql-response+json', async () => {
@@ -238,14 +205,11 @@ export function serverAudits(opts) {
238
205
  ressert(res).status.toBe(200);
239
206
  await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
240
207
  }),
241
- ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`E3E${index}`,
242
- // TODO: convert to MUST after watershed
243
- `SHOULD use 400 status code on ${extendedTypeof(invalid)} {operationName} parameter when accepting application/graphql-response+json`, async () => {
208
+ ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`6C0${index}`, `MAY use 400 status code on ${extendedTypeof(invalid)} {operationName} parameter`, async () => {
244
209
  const res = await fetchFn(await getUrl(opts.url), {
245
210
  method: 'POST',
246
211
  headers: {
247
212
  'content-type': 'application/json',
248
- accept: 'application/graphql-response+json',
249
213
  },
250
214
  body: JSON.stringify({
251
215
  operationName: invalid,
@@ -254,21 +218,6 @@ export function serverAudits(opts) {
254
218
  });
255
219
  ressert(res).status.toBe(400);
256
220
  })),
257
- ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`FB9${index}`, `SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {operationName} parameter when accepting application/json`, async () => {
258
- const res = await fetchFn(await getUrl(opts.url), {
259
- method: 'POST',
260
- headers: {
261
- 'content-type': 'application/json',
262
- accept: 'application/json',
263
- },
264
- body: JSON.stringify({
265
- operationName: invalid,
266
- query: '{ __typename }',
267
- }),
268
- });
269
- ressert(res).status.toBe(200);
270
- await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
271
- })),
272
221
  audit(
273
222
  // TODO: convert to MUST after watershed
274
223
  '8161', 'SHOULD allow string {operationName} parameter when accepting application/graphql-response+json', async () => {
@@ -334,14 +283,11 @@ export function serverAudits(opts) {
334
283
  await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
335
284
  }),
336
285
  ]),
337
- ...['string', 0, false, ['array']].map((invalid, index) => audit(`69B${index}`,
338
- // TODO: convert to MUST after watershed
339
- `SHOULD use 400 status code on ${extendedTypeof(invalid)} {variables} parameter when accepting application/graphql-response+json`, async () => {
286
+ ...['string', 0, false, ['array']].map((invalid, index) => audit(`476${index}`, `MAY use 400 status code on ${extendedTypeof(invalid)} {variables} parameter`, async () => {
340
287
  const res = await fetchFn(await getUrl(opts.url), {
341
288
  method: 'POST',
342
289
  headers: {
343
290
  'content-type': 'application/json',
344
- accept: 'application/graphql-response+json',
345
291
  },
346
292
  body: JSON.stringify({
347
293
  query: '{ __typename }',
@@ -350,21 +296,6 @@ export function serverAudits(opts) {
350
296
  });
351
297
  ressert(res).status.toBe(400);
352
298
  })),
353
- ...['string', 0, false, ['array']].map((invalid, index) => audit(`F05${index}`, `SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {variables} parameter when accepting application/json`, async () => {
354
- const res = await fetchFn(await getUrl(opts.url), {
355
- method: 'POST',
356
- headers: {
357
- 'content-type': 'application/json',
358
- accept: 'application/json',
359
- },
360
- body: JSON.stringify({
361
- query: '{ __typename }',
362
- variables: invalid,
363
- }),
364
- });
365
- ressert(res).status.toBe(200);
366
- await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
367
- })),
368
299
  audit(
369
300
  // TODO: convert to MUST after watershed
370
301
  '2EA1', 'SHOULD allow map {variables} parameter when accepting application/graphql-response+json', async () => {
@@ -421,14 +352,13 @@ export function serverAudits(opts) {
421
352
  ressert(res).status.toBe(200);
422
353
  await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
423
354
  }),
424
- ...['string', 0, false, ['array']].map((invalid, index) => audit(`904${index}`,
355
+ ...['string', 0, false, ['array']].map((invalid, index) => audit(`58B${index}`,
425
356
  // TODO: convert to MUST after watershed
426
- `SHOULD use 400 status code on ${extendedTypeof(invalid)} {extensions} parameter when accepting application/graphql-response+json`, async () => {
357
+ `MAY use 400 status code on ${extendedTypeof(invalid)} {extensions} parameter`, async () => {
427
358
  const res = await fetchFn(await getUrl(opts.url), {
428
359
  method: 'POST',
429
360
  headers: {
430
361
  'content-type': 'application/json',
431
- accept: 'application/graphql-response+json',
432
362
  },
433
363
  body: JSON.stringify({
434
364
  query: '{ __typename }',
@@ -437,21 +367,6 @@ export function serverAudits(opts) {
437
367
  });
438
368
  ressert(res).status.toBe(400);
439
369
  })),
440
- ...['string', 0, false, ['array']].map((invalid, index) => audit(`368${index}`, `SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {extensions} parameter when accepting application/json`, async () => {
441
- const res = await fetchFn(await getUrl(opts.url), {
442
- method: 'POST',
443
- headers: {
444
- 'content-type': 'application/json',
445
- accept: 'application/json',
446
- },
447
- body: JSON.stringify({
448
- query: '{ __typename }',
449
- extensions: invalid,
450
- }),
451
- });
452
- ressert(res).status.toBe(200);
453
- await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
454
- })),
455
370
  audit(
456
371
  // TODO: convert to MUST after watershed
457
372
  '428F', 'SHOULD allow map {extensions} parameter when accepting application/graphql-response+json', async () => {
@@ -483,136 +398,91 @@ export function serverAudits(opts) {
483
398
  ressert(res).status.toBe(200);
484
399
  await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
485
400
  }),
486
- // TODO: audit('39AA', 'MUST accept a map for the {extensions} parameter'),
487
- // Response application/json
488
- audit('D477', 'SHOULD use 200 status code on JSON parsing failure when accepting application/json', async () => {
401
+ audit('B6DC', 'MAY use 4xx or 5xx status codes on JSON parsing failure', async () => {
489
402
  const res = await fetchFn(await getUrl(opts.url), {
490
403
  method: 'POST',
491
404
  headers: {
492
405
  'content-type': 'application/json',
493
- accept: 'application/json',
494
406
  },
495
407
  body: '{ "not a JSON',
496
408
  });
497
- ressert(res).status.toBe(200);
498
- }),
499
- audit('F5AF', 'SHOULD use 200 status code if parameters are invalid when accepting application/json', async () => {
500
- const res = await fetchFn(await getUrl(opts.url), {
501
- method: 'POST',
502
- headers: {
503
- 'content-type': 'application/json',
504
- accept: 'application/json',
505
- },
506
- body: JSON.stringify({
507
- qeury: /* typo */ '{ __typename }',
508
- }),
509
- });
510
- ressert(res).status.toBe(200);
409
+ ressert(res).status.toBeBetween(400, 499);
511
410
  }),
512
- audit('572B', 'SHOULD use 200 status code on document parsing failure when accepting application/json', async () => {
411
+ audit('BCF8', 'MAY use 400 status code on JSON parsing failure', async () => {
513
412
  const res = await fetchFn(await getUrl(opts.url), {
514
413
  method: 'POST',
515
414
  headers: {
516
415
  'content-type': 'application/json',
517
- accept: 'application/json',
518
416
  },
519
- body: JSON.stringify({ query: '{' }),
417
+ body: '{ "not a JSON',
520
418
  });
521
- ressert(res).status.toBe(200);
419
+ ressert(res).status.toBe(400);
522
420
  }),
523
- audit('FDE2', 'SHOULD use 200 status code on document validation failure when accepting application/json', async () => {
421
+ audit('8764', 'MAY use 4xx or 5xx status codes if parameters are invalid', async () => {
524
422
  const res = await fetchFn(await getUrl(opts.url), {
525
423
  method: 'POST',
526
424
  headers: {
527
425
  'content-type': 'application/json',
528
- accept: 'application/json',
529
426
  },
530
427
  body: JSON.stringify({
531
- query: '{ 8f31403dfe404bccbb0e835f2629c6a7 }', // making sure the field doesnt exist
428
+ qeury /* typo */: '{ __typename }',
532
429
  }),
533
430
  });
534
- ressert(res).status.toBe(200);
431
+ ressert(res).status.toBeBetween(400, 599);
535
432
  }),
536
- audit('7B9B', 'SHOULD use a status code of 200 on variable coercion failure when accepting application/json', async () => {
433
+ audit('3E3A', 'MAY use 400 status code if parameters are invalid', async () => {
537
434
  const res = await fetchFn(await getUrl(opts.url), {
538
435
  method: 'POST',
539
436
  headers: {
540
437
  'content-type': 'application/json',
541
- accept: 'application/json',
542
438
  },
543
439
  body: JSON.stringify({
544
- query: 'query CoerceFailure($id: ID!){ __typename }',
545
- variables: { id: null },
440
+ qeury: /* typo */ '{ __typename }',
546
441
  }),
547
442
  });
548
- ressert(res).status.toBe(200);
549
- }),
550
- // Response application/graphql-response+json
551
- audit(
552
- // TODO: convert to MUST after watershed
553
- '60AA', 'SHOULD use 4xx or 5xx status codes on JSON parsing failure when accepting application/graphql-response+json', async () => {
554
- const res = await fetchFn(await getUrl(opts.url), {
555
- method: 'POST',
556
- headers: {
557
- 'content-type': 'application/json',
558
- accept: 'application/graphql-response+json',
559
- },
560
- body: '{ "not a JSON',
561
- });
562
- ressert(res).status.toBeBetween(400, 499);
563
- }),
564
- audit('2163', 'SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
565
- const res = await fetchFn(await getUrl(opts.url), {
566
- method: 'POST',
567
- headers: {
568
- 'content-type': 'application/json',
569
- accept: 'application/graphql-response+json',
570
- },
571
- body: '{ "not a JSON',
572
- });
573
443
  ressert(res).status.toBe(400);
574
444
  }),
575
- audit(
576
- // TODO: convert to MUST after watershed
577
- '3E36', 'SHOULD use 4xx or 5xx status codes if parameters are invalid when accepting application/graphql-response+json', async () => {
445
+ // TODO: audit('39AA', 'MUST accept a map for the {extensions} parameter'),
446
+ // Response application/json
447
+ audit('572B', 'SHOULD use 200 status code on document parsing failure when accepting application/json', async () => {
578
448
  const res = await fetchFn(await getUrl(opts.url), {
579
449
  method: 'POST',
580
450
  headers: {
581
451
  'content-type': 'application/json',
582
- accept: 'application/graphql-response+json',
452
+ accept: 'application/json',
583
453
  },
584
- body: JSON.stringify({
585
- qeury /* typo */: '{ __typename }',
586
- }),
454
+ body: JSON.stringify({ query: '{' }),
587
455
  });
588
- ressert(res).status.toBeBetween(400, 599);
456
+ ressert(res).status.toBe(200);
589
457
  }),
590
- audit('17C5', 'SHOULD use 400 status code if parameters are invalid when accepting application/graphql-response+json', async () => {
458
+ audit('FDE2', 'SHOULD use 200 status code on document validation failure when accepting application/json', async () => {
591
459
  const res = await fetchFn(await getUrl(opts.url), {
592
460
  method: 'POST',
593
461
  headers: {
594
462
  'content-type': 'application/json',
595
- accept: 'application/graphql-response+json',
463
+ accept: 'application/json',
596
464
  },
597
465
  body: JSON.stringify({
598
- qeury: /* typo */ '{ __typename }',
466
+ query: '{ 8f31403dfe404bccbb0e835f2629c6a7 }', // making sure the field doesnt exist
599
467
  }),
600
468
  });
601
- ressert(res).status.toBe(400);
469
+ ressert(res).status.toBe(200);
602
470
  }),
603
- audit('34D6', 'SHOULD not contain the data entry if parameters are invalid when accepting application/graphql-response+json', async () => {
471
+ audit('7B9B', 'SHOULD use a status code of 200 on variable coercion failure when accepting application/json', async () => {
604
472
  const res = await fetchFn(await getUrl(opts.url), {
605
473
  method: 'POST',
606
474
  headers: {
607
475
  'content-type': 'application/json',
608
- accept: 'application/graphql-response+json',
476
+ accept: 'application/json',
609
477
  },
610
478
  body: JSON.stringify({
611
- qeury: /* typo */ '{ __typename }',
479
+ query: 'query CoerceFailure($id: ID!){ __typename }',
480
+ variables: { id: null },
612
481
  }),
613
482
  });
614
- await ressert(res).bodyAsExecutionResult.data.toBe(undefined);
483
+ ressert(res).status.toBe(200);
615
484
  }),
485
+ // Response application/graphql-response+json
616
486
  audit(
617
487
  // TODO: convert to MUST after watershed
618
488
  '865D', 'SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json', async () => {
@@ -48,10 +48,13 @@ function audit(id, name, fn) {
48
48
  id,
49
49
  name,
50
50
  status: name.startsWith('MUST')
51
- ? // only failing MUSTs are considered errors
51
+ ? // failing MUSTs are considered errors
52
52
  'error'
53
- : // everything else is optional and considered a warning
54
- 'warn',
53
+ : name.startsWith('SHOULD')
54
+ ? // recommendations are warnings
55
+ 'warn'
56
+ : // everything else is truly optional
57
+ 'notice',
55
58
  reason: err.reason,
56
59
  response: err.response,
57
60
  };
@@ -31,10 +31,13 @@ export function audit(id, name, fn) {
31
31
  id,
32
32
  name,
33
33
  status: name.startsWith('MUST')
34
- ? // only failing MUSTs are considered errors
34
+ ? // failing MUSTs are considered errors
35
35
  'error'
36
- : // everything else is optional and considered a warning
37
- 'warn',
36
+ : name.startsWith('SHOULD')
37
+ ? // recommendations are warnings
38
+ 'warn'
39
+ : // everything else is truly optional
40
+ 'notice',
38
41
  reason: err.reason,
39
42
  response: err.response,
40
43
  };
package/lib/handler.d.mts CHANGED
@@ -101,6 +101,12 @@ export declare function isResponse(val: unknown): val is Response;
101
101
  * @category Server
102
102
  */
103
103
  export type OperationContext = Record<PropertyKey, unknown> | symbol | number | string | boolean | undefined | null;
104
+ /**
105
+ * The (GraphQL) error formatter function.
106
+ *
107
+ * @category Server
108
+ */
109
+ export type FormatError = (err: Readonly<GraphQLError | Error>) => GraphQLError | Error;
104
110
  /** @category Server */
105
111
  export type OperationArgs<Context extends OperationContext = undefined> = ExecutionArgs & {
106
112
  contextValue?: Context;
@@ -215,6 +221,14 @@ export interface HandlerOptions<RequestRaw = unknown, RequestContext = unknown,
215
221
  * further execution.
216
222
  */
217
223
  onOperation?: (req: Request<RequestRaw, RequestContext>, args: OperationArgs<Context>, result: ExecutionResult) => Promise<ExecutionResult | Response | void> | ExecutionResult | Response | void;
224
+ /**
225
+ * Format handled errors to your satisfaction. Either GraphQL errors
226
+ * or safe request processing errors are meant by "handleded errors".
227
+ *
228
+ * If multiple errors have occured, all of them will be mapped using
229
+ * this formatter.
230
+ */
231
+ formatError?: FormatError;
218
232
  }
219
233
  /**
220
234
  * The ready-to-use handler. Simply plug it in your favourite HTTP framework
@@ -301,9 +315,12 @@ export declare function getAcceptableMediaType(acceptHeader: string | null | und
301
315
  *
302
316
  * If the first argument is an `ExecutionResult`, the operation will be treated as "successful".
303
317
  *
304
- * If the first argument is _any_ object without the `data` field, it will be treated as an error (as per the spec)
305
- * and the response will be constructed with the help of `acceptedMediaType` complying with the GraphQL over HTTP spec.
318
+ * If the first argument is (an array of) `GraphQLError`, or an `ExecutionResult` without the `data` field, it will be treated
319
+ * the response will be constructed with the help of `acceptedMediaType` complying with the GraphQL over HTTP spec.
320
+ *
321
+ * If the first argument is an `Error`, the operation will be treated as a bad request responding with `400: Bad Request` and the
322
+ * error will be present in the `ExecutionResult` style.
306
323
  *
307
324
  * @category Server
308
325
  */
309
- export declare function makeResponse(resultOrErrors: Readonly<ExecutionResult> | Readonly<GraphQLError[]> | Readonly<GraphQLError>, acceptedMediaType: AcceptableMediaType): Response;
326
+ export declare function makeResponse(resultOrErrors: Readonly<ExecutionResult> | Readonly<GraphQLError[]> | Readonly<GraphQLError> | Readonly<Error>, acceptedMediaType: AcceptableMediaType, formatError: FormatError): Response;
package/lib/handler.d.ts CHANGED
@@ -101,6 +101,12 @@ export declare function isResponse(val: unknown): val is Response;
101
101
  * @category Server
102
102
  */
103
103
  export type OperationContext = Record<PropertyKey, unknown> | symbol | number | string | boolean | undefined | null;
104
+ /**
105
+ * The (GraphQL) error formatter function.
106
+ *
107
+ * @category Server
108
+ */
109
+ export type FormatError = (err: Readonly<GraphQLError | Error>) => GraphQLError | Error;
104
110
  /** @category Server */
105
111
  export type OperationArgs<Context extends OperationContext = undefined> = ExecutionArgs & {
106
112
  contextValue?: Context;
@@ -215,6 +221,14 @@ export interface HandlerOptions<RequestRaw = unknown, RequestContext = unknown,
215
221
  * further execution.
216
222
  */
217
223
  onOperation?: (req: Request<RequestRaw, RequestContext>, args: OperationArgs<Context>, result: ExecutionResult) => Promise<ExecutionResult | Response | void> | ExecutionResult | Response | void;
224
+ /**
225
+ * Format handled errors to your satisfaction. Either GraphQL errors
226
+ * or safe request processing errors are meant by "handleded errors".
227
+ *
228
+ * If multiple errors have occured, all of them will be mapped using
229
+ * this formatter.
230
+ */
231
+ formatError?: FormatError;
218
232
  }
219
233
  /**
220
234
  * The ready-to-use handler. Simply plug it in your favourite HTTP framework
@@ -301,9 +315,12 @@ export declare function getAcceptableMediaType(acceptHeader: string | null | und
301
315
  *
302
316
  * If the first argument is an `ExecutionResult`, the operation will be treated as "successful".
303
317
  *
304
- * If the first argument is _any_ object without the `data` field, it will be treated as an error (as per the spec)
305
- * and the response will be constructed with the help of `acceptedMediaType` complying with the GraphQL over HTTP spec.
318
+ * If the first argument is (an array of) `GraphQLError`, or an `ExecutionResult` without the `data` field, it will be treated
319
+ * the response will be constructed with the help of `acceptedMediaType` complying with the GraphQL over HTTP spec.
320
+ *
321
+ * If the first argument is an `Error`, the operation will be treated as a bad request responding with `400: Bad Request` and the
322
+ * error will be present in the `ExecutionResult` style.
306
323
  *
307
324
  * @category Server
308
325
  */
309
- export declare function makeResponse(resultOrErrors: Readonly<ExecutionResult> | Readonly<GraphQLError[]> | Readonly<GraphQLError>, acceptedMediaType: AcceptableMediaType): Response;
326
+ export declare function makeResponse(resultOrErrors: Readonly<ExecutionResult> | Readonly<GraphQLError[]> | Readonly<GraphQLError> | Readonly<Error>, acceptedMediaType: AcceptableMediaType, formatError: FormatError): Response;
package/lib/handler.js CHANGED
@@ -76,7 +76,7 @@ exports.isResponse = isResponse;
76
76
  * @category Server
77
77
  */
78
78
  function createHandler(options) {
79
- const { schema, context, validate = graphql_1.validate, validationRules = [], execute = graphql_1.execute, parse = graphql_1.parse, getOperationAST = graphql_1.getOperationAST, rootValue, onSubscribe, onOperation, } = options;
79
+ const { schema, context, validate = graphql_1.validate, validationRules = [], execute = graphql_1.execute, parse = graphql_1.parse, getOperationAST = graphql_1.getOperationAST, rootValue, onSubscribe, onOperation, formatError = (err) => err, } = options;
80
80
  return async function handler(req) {
81
81
  var _a, _b;
82
82
  const method = req.method;
@@ -177,6 +177,10 @@ function createHandler(options) {
177
177
  Array.isArray(partParams.variables))) {
178
178
  throw new Error('Invalid variables');
179
179
  }
180
+ if (partParams.operationName != null &&
181
+ typeof partParams.operationName !== 'string') {
182
+ throw new Error('Invalid operationName');
183
+ }
180
184
  if (partParams.extensions != null &&
181
185
  (typeof partParams.extensions !== 'object' ||
182
186
  Array.isArray(partParams.extensions))) {
@@ -186,7 +190,7 @@ function createHandler(options) {
186
190
  params = partParams;
187
191
  }
188
192
  catch (err) {
189
- return makeResponse(new graphql_1.GraphQLError(err.message), acceptedMediaType);
193
+ return makeResponse(err, acceptedMediaType, formatError);
190
194
  }
191
195
  let args;
192
196
  const maybeResErrsOrArgs = await (onSubscribe === null || onSubscribe === void 0 ? void 0 : onSubscribe(req, params));
@@ -194,7 +198,7 @@ function createHandler(options) {
194
198
  return maybeResErrsOrArgs;
195
199
  else if ((0, utils_1.isExecutionResult)(maybeResErrsOrArgs) ||
196
200
  (0, utils_1.areGraphQLErrors)(maybeResErrsOrArgs))
197
- return makeResponse(maybeResErrsOrArgs, acceptedMediaType);
201
+ return makeResponse(maybeResErrsOrArgs, acceptedMediaType, formatError);
198
202
  else if (maybeResErrsOrArgs)
199
203
  args = maybeResErrsOrArgs;
200
204
  else {
@@ -206,7 +210,7 @@ function createHandler(options) {
206
210
  document = parse(query);
207
211
  }
208
212
  catch (err) {
209
- return makeResponse(err, acceptedMediaType);
213
+ return makeResponse(err, acceptedMediaType, formatError);
210
214
  }
211
215
  const resOrContext = typeof context === 'function' ? await context(req, params) : context;
212
216
  if (isResponse(resOrContext))
@@ -235,7 +239,7 @@ function createHandler(options) {
235
239
  }
236
240
  const validationErrs = validate(args.schema, args.document, rules);
237
241
  if (validationErrs.length) {
238
- return makeResponse(validationErrs, acceptedMediaType);
242
+ return makeResponse(validationErrs, acceptedMediaType, formatError);
239
243
  }
240
244
  }
241
245
  let operation;
@@ -246,10 +250,10 @@ function createHandler(options) {
246
250
  operation = ast.operation;
247
251
  }
248
252
  catch (_d) {
249
- return makeResponse(new graphql_1.GraphQLError('Unable to detect operation AST'), acceptedMediaType);
253
+ return makeResponse(new graphql_1.GraphQLError('Unable to detect operation AST'), acceptedMediaType, formatError);
250
254
  }
251
255
  if (operation === 'subscription') {
252
- return makeResponse(new graphql_1.GraphQLError('Subscriptions are not supported'), acceptedMediaType);
256
+ return makeResponse(new graphql_1.GraphQLError('Subscriptions are not supported'), acceptedMediaType, formatError);
253
257
  }
254
258
  // mutations cannot happen over GETs
255
259
  // https://graphql.github.io/graphql-over-http/draft/#sel-CALFJRPAAELBAAxwP
@@ -283,9 +287,9 @@ function createHandler(options) {
283
287
  else if (maybeResponseOrResult)
284
288
  result = maybeResponseOrResult;
285
289
  if ((0, utils_1.isAsyncIterable)(result)) {
286
- return makeResponse(new graphql_1.GraphQLError('Subscriptions are not supported'), acceptedMediaType);
290
+ return makeResponse(new graphql_1.GraphQLError('Subscriptions are not supported'), acceptedMediaType, formatError);
287
291
  }
288
- return makeResponse(result, acceptedMediaType);
292
+ return makeResponse(result, acceptedMediaType, formatError);
289
293
  };
290
294
  }
291
295
  exports.createHandler = createHandler;
@@ -327,12 +331,29 @@ exports.getAcceptableMediaType = getAcceptableMediaType;
327
331
  *
328
332
  * If the first argument is an `ExecutionResult`, the operation will be treated as "successful".
329
333
  *
330
- * If the first argument is _any_ object without the `data` field, it will be treated as an error (as per the spec)
331
- * and the response will be constructed with the help of `acceptedMediaType` complying with the GraphQL over HTTP spec.
334
+ * If the first argument is (an array of) `GraphQLError`, or an `ExecutionResult` without the `data` field, it will be treated
335
+ * the response will be constructed with the help of `acceptedMediaType` complying with the GraphQL over HTTP spec.
336
+ *
337
+ * If the first argument is an `Error`, the operation will be treated as a bad request responding with `400: Bad Request` and the
338
+ * error will be present in the `ExecutionResult` style.
332
339
  *
333
340
  * @category Server
334
341
  */
335
- function makeResponse(resultOrErrors, acceptedMediaType) {
342
+ function makeResponse(resultOrErrors, acceptedMediaType, formatError) {
343
+ if (resultOrErrors instanceof Error &&
344
+ // because GraphQLError extends the Error class
345
+ !(0, utils_1.isGraphQLError)(resultOrErrors)) {
346
+ return [
347
+ JSON.stringify({ errors: [formatError(resultOrErrors)] }, utils_1.jsonErrorReplacer),
348
+ {
349
+ status: 400,
350
+ statusText: 'Bad Request',
351
+ headers: {
352
+ 'content-type': 'application/json; charset=utf-8',
353
+ },
354
+ },
355
+ ];
356
+ }
336
357
  const errors = (0, utils_1.isGraphQLError)(resultOrErrors)
337
358
  ? [resultOrErrors]
338
359
  : (0, utils_1.areGraphQLErrors)(resultOrErrors)
@@ -340,7 +361,7 @@ function makeResponse(resultOrErrors, acceptedMediaType) {
340
361
  : null;
341
362
  if (errors) {
342
363
  return [
343
- JSON.stringify({ errors }),
364
+ JSON.stringify({ errors: errors.map(formatError) }, utils_1.jsonErrorReplacer),
344
365
  Object.assign(Object.assign({}, (acceptedMediaType === 'application/json'
345
366
  ? {
346
367
  status: 200,
@@ -357,7 +378,8 @@ function makeResponse(resultOrErrors, acceptedMediaType) {
357
378
  ];
358
379
  }
359
380
  return [
360
- JSON.stringify(resultOrErrors),
381
+ JSON.stringify('errors' in resultOrErrors && resultOrErrors.errors
382
+ ? Object.assign(Object.assign({}, resultOrErrors), { errors: resultOrErrors.errors.map(formatError) }) : resultOrErrors, utils_1.jsonErrorReplacer),
361
383
  {
362
384
  status: 200,
363
385
  statusText: 'OK',