graphql-http 1.22.4 → 1.23.1

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.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  <h6>Simple, pluggable, zero-dependency, <a href="https://graphql.github.io/graphql-over-http">GraphQL over HTTP spec</a> compliant server, client and audit suite.</h6>
7
7
 
8
- [![Continuous integration](https://github.com/graphql/graphql-http/workflows/Continuous%20integration/badge.svg)](https://github.com/graphql/graphql-http/actions?query=workflow%3A%22Continuous+integration%22) [![graphql-http](https://img.shields.io/npm/v/graphql-http.svg?label=graphql-http&logo=npm)](https://www.npmjs.com/package/graphql-http)
8
+ [![CI](https://github.com/graphql/graphql-http/workflows/CI/badge.svg)](https://github.com/graphql/graphql-http/actions?query=workflow%3A%22Continuous+integration%22) [![graphql-http](https://img.shields.io/npm/v/graphql-http.svg?label=graphql-http&logo=npm)](https://www.npmjs.com/package/graphql-http)
9
9
 
10
10
  <i>Quickly check for compliance? Visit <b>[graphql-http.com](https://graphql-http.com)</b>!</i>
11
11
 
@@ -98,10 +98,10 @@ async function printAuditFail(result) {
98
98
  const headers = {};
99
99
  for (const [key, val] of res.headers.entries()) {
100
100
  // some headers change on each run, dont report it
101
- if (key === 'date') {
101
+ if (['date', 'expires'].includes(key)) {
102
102
  headers[key] = '<timestamp>';
103
103
  }
104
- else if (['cf-ray', 'server-timing', 'set-cookie'].includes(key)) {
104
+ else if (['cf-ray', 'server-timing', 'set-cookie', 'age'].includes(key)) {
105
105
  headers[key] = '<omitted>';
106
106
  }
107
107
  else {
@@ -94,10 +94,10 @@ async function printAuditFail(result) {
94
94
  const headers = {};
95
95
  for (const [key, val] of res.headers.entries()) {
96
96
  // some headers change on each run, dont report it
97
- if (key === 'date') {
97
+ if (['date', 'expires'].includes(key)) {
98
98
  headers[key] = '<timestamp>';
99
99
  }
100
- else if (['cf-ray', 'server-timing', 'set-cookie'].includes(key)) {
100
+ else if (['cf-ray', 'server-timing', 'set-cookie', 'age'].includes(key)) {
101
101
  headers[key] = '<omitted>';
102
102
  }
103
103
  else {
@@ -134,9 +134,10 @@ function serverAudits(opts) {
134
134
  (0, utils_1.ressert)(res).status.toBeBetween(400, 499);
135
135
  }),
136
136
  // Request POST
137
- (0, utils_1.audit)('9ABE', 'MAY respond with 4xx status code if content-type is not supplied on POST requests', async () => {
137
+ (0, utils_1.audit)('9ABE', 'SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
138
138
  const res = await fetchFn(await getUrl(opts.url), {
139
139
  method: 'POST',
140
+ body: JSON.stringify({ query: '{ __typename }' }),
140
141
  });
141
142
  (0, utils_1.ressert)(res).status.toBeBetween(400, 499);
142
143
  }),
@@ -341,6 +342,7 @@ function serverAudits(opts) {
341
342
  },
342
343
  });
343
344
  (0, utils_1.ressert)(res).status.toBe(200);
345
+ await (0, utils_1.ressert)(res).bodyAsExecutionResult.notToHaveProperty('errors');
344
346
  }),
345
347
  (0, utils_1.audit)('6A70', 'MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json', async () => {
346
348
  const url = new URL(await getUrl(opts.url));
@@ -401,21 +403,38 @@ function serverAudits(opts) {
401
403
  (0, utils_1.ressert)(res).status.toBe(200);
402
404
  await (0, utils_1.ressert)(res).bodyAsExecutionResult.notToHaveProperty('errors');
403
405
  }),
404
- (0, utils_1.audit)('B6DC', 'MAY use 4xx or 5xx status codes on JSON parsing failure', async () => {
406
+ (0, utils_1.audit)('B6DC', 'MAY use 2xx, 4xx, or 5xx status codes on JSON parsing failure when accepting application/json', async () => {
405
407
  const res = await fetchFn(await getUrl(opts.url), {
406
408
  method: 'POST',
407
409
  headers: {
408
410
  'content-type': 'application/json',
411
+ accept: 'application/json',
409
412
  },
410
413
  body: '{ "not a JSON',
411
414
  });
412
- (0, utils_1.ressert)(res).status.toBeBetween(400, 499);
415
+ (0, utils_1.ressert)(res).status.toBeBetweenMultiple([
416
+ [200, 299],
417
+ [400, 499],
418
+ [500, 599],
419
+ ]);
413
420
  }),
414
- (0, utils_1.audit)('BCF8', 'MAY use 400 status code on JSON parsing failure', async () => {
421
+ (0, utils_1.audit)('BCF8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/json', async () => {
415
422
  const res = await fetchFn(await getUrl(opts.url), {
416
423
  method: 'POST',
417
424
  headers: {
418
425
  'content-type': 'application/json',
426
+ accept: 'application/json',
427
+ },
428
+ body: '{ "not a JSON',
429
+ });
430
+ (0, utils_1.ressert)(res).status.toBe(400);
431
+ }),
432
+ (0, utils_1.audit)('B7N8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
433
+ const res = await fetchFn(await getUrl(opts.url), {
434
+ method: 'POST',
435
+ headers: {
436
+ 'content-type': 'application/json',
437
+ accept: 'application/graphql-response+json',
419
438
  },
420
439
  body: '{ "not a JSON',
421
440
  });
@@ -131,9 +131,10 @@ export function serverAudits(opts) {
131
131
  ressert(res).status.toBeBetween(400, 499);
132
132
  }),
133
133
  // Request POST
134
- audit('9ABE', 'MAY respond with 4xx status code if content-type is not supplied on POST requests', async () => {
134
+ audit('9ABE', 'SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
135
135
  const res = await fetchFn(await getUrl(opts.url), {
136
136
  method: 'POST',
137
+ body: JSON.stringify({ query: '{ __typename }' }),
137
138
  });
138
139
  ressert(res).status.toBeBetween(400, 499);
139
140
  }),
@@ -338,6 +339,7 @@ export function serverAudits(opts) {
338
339
  },
339
340
  });
340
341
  ressert(res).status.toBe(200);
342
+ await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
341
343
  }),
342
344
  audit('6A70', 'MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json', async () => {
343
345
  const url = new URL(await getUrl(opts.url));
@@ -398,21 +400,38 @@ export function serverAudits(opts) {
398
400
  ressert(res).status.toBe(200);
399
401
  await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
400
402
  }),
401
- audit('B6DC', 'MAY use 4xx or 5xx status codes on JSON parsing failure', async () => {
403
+ audit('B6DC', 'MAY use 2xx, 4xx, or 5xx status codes on JSON parsing failure when accepting application/json', async () => {
402
404
  const res = await fetchFn(await getUrl(opts.url), {
403
405
  method: 'POST',
404
406
  headers: {
405
407
  'content-type': 'application/json',
408
+ accept: 'application/json',
406
409
  },
407
410
  body: '{ "not a JSON',
408
411
  });
409
- ressert(res).status.toBeBetween(400, 499);
412
+ ressert(res).status.toBeBetweenMultiple([
413
+ [200, 299],
414
+ [400, 499],
415
+ [500, 599],
416
+ ]);
410
417
  }),
411
- audit('BCF8', 'MAY use 400 status code on JSON parsing failure', async () => {
418
+ audit('BCF8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/json', async () => {
412
419
  const res = await fetchFn(await getUrl(opts.url), {
413
420
  method: 'POST',
414
421
  headers: {
415
422
  'content-type': 'application/json',
423
+ accept: 'application/json',
424
+ },
425
+ body: '{ "not a JSON',
426
+ });
427
+ ressert(res).status.toBe(400);
428
+ }),
429
+ audit('B7N8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
430
+ const res = await fetchFn(await getUrl(opts.url), {
431
+ method: 'POST',
432
+ headers: {
433
+ 'content-type': 'application/json',
434
+ accept: 'application/graphql-response+json',
416
435
  },
417
436
  body: '{ "not a JSON',
418
437
  });
@@ -41,6 +41,7 @@ export declare function ressert(res: Response): {
41
41
  status: {
42
42
  toBe(code: number): void;
43
43
  toBeBetween: (min: number, max: number) => void;
44
+ toBeBetweenMultiple: (ranges: Array<[number, number]>) => void;
44
45
  };
45
46
  header(key: 'content-type'): {
46
47
  toContain(part: string): void;
@@ -41,6 +41,7 @@ export declare function ressert(res: Response): {
41
41
  status: {
42
42
  toBe(code: number): void;
43
43
  toBeBetween: (min: number, max: number) => void;
44
+ toBeBetweenMultiple: (ranges: Array<[number, number]>) => void;
44
45
  };
45
46
  header(key: 'content-type'): {
46
47
  toContain(part: string): void;
@@ -97,6 +97,14 @@ function ressert(res) {
97
97
  throw new AuditError(res, `Response status is not between ${min} and ${max}`);
98
98
  }
99
99
  },
100
+ toBeBetweenMultiple: (ranges) => {
101
+ const isInRange = ranges.some(([min, max]) => min <= res.status && res.status <= max);
102
+ if (!isInRange) {
103
+ throw new AuditError(res, `Response status is not between any of the provided ranges: ${ranges
104
+ .map(([min, max]) => `[${min}, ${max}]`)
105
+ .join(', ')}`);
106
+ }
107
+ },
100
108
  },
101
109
  header(key) {
102
110
  return {
@@ -78,6 +78,14 @@ export function ressert(res) {
78
78
  throw new AuditError(res, `Response status is not between ${min} and ${max}`);
79
79
  }
80
80
  },
81
+ toBeBetweenMultiple: (ranges) => {
82
+ const isInRange = ranges.some(([min, max]) => min <= res.status && res.status <= max);
83
+ if (!isInRange) {
84
+ throw new AuditError(res, `Response status is not between any of the provided ranges: ${ranges
85
+ .map(([min, max]) => `[${min}, ${max}]`)
86
+ .join(', ')}`);
87
+ }
88
+ },
81
89
  },
82
90
  header(key) {
83
91
  return {
package/lib/handler.d.mts CHANGED
@@ -201,7 +201,7 @@ export interface HandlerOptions<RequestRaw = unknown, RequestContext = unknown,
201
201
  getOperationAST?: typeof graphqlGetOperationAST;
202
202
  /**
203
203
  * The GraphQL root value or resolvers to go alongside the execution.
204
- * Learn more about them here: https://graphql.org/learn/execution/#root-fields-resolvers.
204
+ * Learn more about them here: https://graphql.org/learn/execution/#root-fields-and-resolvers.
205
205
  *
206
206
  * If you return from `onSubscribe`, and the returned value is
207
207
  * missing the `rootValue` field, the relevant operation root
package/lib/handler.d.ts CHANGED
@@ -201,7 +201,7 @@ export interface HandlerOptions<RequestRaw = unknown, RequestContext = unknown,
201
201
  getOperationAST?: typeof graphqlGetOperationAST;
202
202
  /**
203
203
  * The GraphQL root value or resolvers to go alongside the execution.
204
- * Learn more about them here: https://graphql.org/learn/execution/#root-fields-resolvers.
204
+ * Learn more about them here: https://graphql.org/learn/execution/#root-fields-and-resolvers.
205
205
  *
206
206
  * If you return from `onSubscribe`, and the returned value is
207
207
  * missing the `rootValue` field, the relevant operation root
package/lib/handler.js CHANGED
@@ -308,7 +308,7 @@ function createHandler(options) {
308
308
  return makeResponse(new graphql_1.GraphQLError('Subscriptions are not supported'), acceptedMediaType, formatError);
309
309
  }
310
310
  // mutations cannot happen over GETs
311
- // https://graphql.github.io/graphql-over-http/draft/#sel-CALFJRPAAELBAAxwP
311
+ // https://graphql.github.io/graphql-over-http/draft/#sel-GALJTPABABgCzsd
312
312
  if (operation === 'mutation' && req.method === 'GET') {
313
313
  return [
314
314
  JSON.stringify({
package/lib/handler.mjs CHANGED
@@ -304,7 +304,7 @@ export function createHandler(options) {
304
304
  return makeResponse(new GraphQLError('Subscriptions are not supported'), acceptedMediaType, formatError);
305
305
  }
306
306
  // mutations cannot happen over GETs
307
- // https://graphql.github.io/graphql-over-http/draft/#sel-CALFJRPAAELBAAxwP
307
+ // https://graphql.github.io/graphql-over-http/draft/#sel-GALJTPABABgCzsd
308
308
  if (operation === 'mutation' && req.method === 'GET') {
309
309
  return [
310
310
  JSON.stringify({
@@ -7,6 +7,12 @@ import { RequestParams } from '../common.mjs';
7
7
  * @category Server/express
8
8
  */
9
9
  export interface RequestContext {
10
+ /**
11
+ * The Express response object for the current request.
12
+ *
13
+ * This can be used to manipulate the HTTP response, for example to set
14
+ * response headers or cookies.
15
+ */
10
16
  res: Response;
11
17
  }
12
18
  /**
@@ -59,16 +65,40 @@ export type HandlerOptions<Context extends OperationContext = undefined> = RawHa
59
65
  * Create a GraphQL over HTTP spec compliant request handler for
60
66
  * the express framework.
61
67
  *
68
+ * The Express response is available through `req.context.res`. To expose it
69
+ * to GraphQL resolvers, return it from the `context` option. This can be used
70
+ * to manipulate the HTTP response, for example to set response headers or
71
+ * cookies.
72
+ *
62
73
  * ```js
63
74
  * import express from 'express'; // yarn add express
64
75
  * import { createHandler } from 'graphql-http/lib/use/express';
65
76
  * import { schema } from './my-graphql-schema/index.mjs';
66
77
  *
67
78
  * const app = express();
68
- * app.all('/graphql', createHandler({ schema }));
79
+ *
80
+ * app.all(
81
+ * '/graphql',
82
+ * createHandler({
83
+ * schema,
84
+ * context(req) {
85
+ * return {
86
+ * res: req.context.res,
87
+ * };
88
+ * },
89
+ * }),
90
+ * );
69
91
  *
70
92
  * app.listen({ port: 4000 });
71
- * console.log('Listening to port 4000');
93
+ * ```
94
+ *
95
+ * A resolver can then manipulate the response:
96
+ *
97
+ * ```js
98
+ * resolve(_, __, { res }) {
99
+ * res.setHeader('set-cookie', 'session=; Max-Age=0; Path=/');
100
+ * return true;
101
+ * }
72
102
  * ```
73
103
  *
74
104
  * @category Server/express
@@ -7,6 +7,12 @@ import { RequestParams } from '../common';
7
7
  * @category Server/express
8
8
  */
9
9
  export interface RequestContext {
10
+ /**
11
+ * The Express response object for the current request.
12
+ *
13
+ * This can be used to manipulate the HTTP response, for example to set
14
+ * response headers or cookies.
15
+ */
10
16
  res: Response;
11
17
  }
12
18
  /**
@@ -59,16 +65,40 @@ export type HandlerOptions<Context extends OperationContext = undefined> = RawHa
59
65
  * Create a GraphQL over HTTP spec compliant request handler for
60
66
  * the express framework.
61
67
  *
68
+ * The Express response is available through `req.context.res`. To expose it
69
+ * to GraphQL resolvers, return it from the `context` option. This can be used
70
+ * to manipulate the HTTP response, for example to set response headers or
71
+ * cookies.
72
+ *
62
73
  * ```js
63
74
  * import express from 'express'; // yarn add express
64
75
  * import { createHandler } from 'graphql-http/lib/use/express';
65
76
  * import { schema } from './my-graphql-schema';
66
77
  *
67
78
  * const app = express();
68
- * app.all('/graphql', createHandler({ schema }));
79
+ *
80
+ * app.all(
81
+ * '/graphql',
82
+ * createHandler({
83
+ * schema,
84
+ * context(req) {
85
+ * return {
86
+ * res: req.context.res,
87
+ * };
88
+ * },
89
+ * }),
90
+ * );
69
91
  *
70
92
  * app.listen({ port: 4000 });
71
- * console.log('Listening to port 4000');
93
+ * ```
94
+ *
95
+ * A resolver can then manipulate the response:
96
+ *
97
+ * ```js
98
+ * resolve(_, __, { res }) {
99
+ * res.setHeader('set-cookie', 'session=; Max-Age=0; Path=/');
100
+ * return true;
101
+ * }
72
102
  * ```
73
103
  *
74
104
  * @category Server/express
@@ -56,16 +56,40 @@ exports.parseRequestParams = parseRequestParams;
56
56
  * Create a GraphQL over HTTP spec compliant request handler for
57
57
  * the express framework.
58
58
  *
59
+ * The Express response is available through `req.context.res`. To expose it
60
+ * to GraphQL resolvers, return it from the `context` option. This can be used
61
+ * to manipulate the HTTP response, for example to set response headers or
62
+ * cookies.
63
+ *
59
64
  * ```js
60
65
  * import express from 'express'; // yarn add express
61
66
  * import { createHandler } from 'graphql-http/lib/use/express';
62
67
  * import { schema } from './my-graphql-schema';
63
68
  *
64
69
  * const app = express();
65
- * app.all('/graphql', createHandler({ schema }));
70
+ *
71
+ * app.all(
72
+ * '/graphql',
73
+ * createHandler({
74
+ * schema,
75
+ * context(req) {
76
+ * return {
77
+ * res: req.context.res,
78
+ * };
79
+ * },
80
+ * }),
81
+ * );
66
82
  *
67
83
  * app.listen({ port: 4000 });
68
- * console.log('Listening to port 4000');
84
+ * ```
85
+ *
86
+ * A resolver can then manipulate the response:
87
+ *
88
+ * ```js
89
+ * resolve(_, __, { res }) {
90
+ * res.setHeader('set-cookie', 'session=; Max-Age=0; Path=/');
91
+ * return true;
92
+ * }
69
93
  * ```
70
94
  *
71
95
  * @category Server/express
@@ -52,16 +52,40 @@ export async function parseRequestParams(req, res) {
52
52
  * Create a GraphQL over HTTP spec compliant request handler for
53
53
  * the express framework.
54
54
  *
55
+ * The Express response is available through `req.context.res`. To expose it
56
+ * to GraphQL resolvers, return it from the `context` option. This can be used
57
+ * to manipulate the HTTP response, for example to set response headers or
58
+ * cookies.
59
+ *
55
60
  * ```js
56
61
  * import express from 'express'; // yarn add express
57
62
  * import { createHandler } from 'graphql-http/lib/use/express';
58
63
  * import { schema } from './my-graphql-schema/index.mjs';
59
64
  *
60
65
  * const app = express();
61
- * app.all('/graphql', createHandler({ schema }));
66
+ *
67
+ * app.all(
68
+ * '/graphql',
69
+ * createHandler({
70
+ * schema,
71
+ * context(req) {
72
+ * return {
73
+ * res: req.context.res,
74
+ * };
75
+ * },
76
+ * }),
77
+ * );
62
78
  *
63
79
  * app.listen({ port: 4000 });
64
- * console.log('Listening to port 4000');
80
+ * ```
81
+ *
82
+ * A resolver can then manipulate the response:
83
+ *
84
+ * ```js
85
+ * resolve(_, __, { res }) {
86
+ * res.setHeader('set-cookie', 'session=; Max-Age=0; Path=/');
87
+ * return true;
88
+ * }
65
89
  * ```
66
90
  *
67
91
  * @category Server/express
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphql-http",
3
- "version": "1.22.4",
3
+ "version": "1.23.1",
4
4
  "description": "Simple, pluggable, zero-dependency, GraphQL over HTTP spec compliant server, client and audit suite.",
5
5
  "keywords": [
6
6
  "graphql",
@@ -113,7 +113,7 @@
113
113
  "implementations/**/*"
114
114
  ],
115
115
  "peerDependencies": {
116
- "graphql": ">=0.11 <=16"
116
+ "graphql": ">=0.11 <=17"
117
117
  },
118
118
  "devDependencies": {
119
119
  "@cspell/cspell-types": "^8.1.2",
@@ -126,7 +126,6 @@
126
126
  "@types/express": "^4.17.21",
127
127
  "@types/glob": "^8.1.0",
128
128
  "@types/html-validator": "^5.0.6",
129
- "@types/k6": "^0.47.3",
130
129
  "@types/koa": "^2.15.0",
131
130
  "@types/koa-mount": "^4.0.5",
132
131
  "@typescript-eslint/eslint-plugin": "^6.13.2",
@@ -138,7 +137,7 @@
138
137
  "express": "^4.18.2",
139
138
  "fastify": "^4.24.3",
140
139
  "glob": "^10.3.10",
141
- "graphql": "^16.8.1",
140
+ "graphql": "^17.0.0",
142
141
  "html-validator": "^6.0.1",
143
142
  "koa": "^2.15.3",
144
143
  "koa-mount": "^4.0.0",
@@ -152,7 +151,7 @@
152
151
  "typedoc": "^0.25.4",
153
152
  "typedoc-plugin-markdown": "^3.17.1",
154
153
  "typescript": "^5.3.2",
155
- "uWebSockets.js": "uNetworking/uWebSockets.js#v20.34.0",
154
+ "uWebSockets.js": "uNetworking/uWebSockets.js#v20.69.0",
156
155
  "vitest": "^1.0.1"
157
156
  }
158
157
  }
@@ -99,6 +99,14 @@
99
99
  throw new AuditError(res, `Response status is not between ${min} and ${max}`);
100
100
  }
101
101
  },
102
+ toBeBetweenMultiple: (ranges) => {
103
+ const isInRange = ranges.some(([min, max]) => min <= res.status && res.status <= max);
104
+ if (!isInRange) {
105
+ throw new AuditError(res, `Response status is not between any of the provided ranges: ${ranges
106
+ .map(([min, max]) => `[${min}, ${max}]`)
107
+ .join(', ')}`);
108
+ }
109
+ },
102
110
  },
103
111
  header(key) {
104
112
  return {
@@ -296,9 +304,10 @@
296
304
  ressert(res).status.toBeBetween(400, 499);
297
305
  }),
298
306
  // Request POST
299
- audit('9ABE', 'MAY respond with 4xx status code if content-type is not supplied on POST requests', async () => {
307
+ audit('9ABE', 'SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
300
308
  const res = await fetchFn(await getUrl(opts.url), {
301
309
  method: 'POST',
310
+ body: JSON.stringify({ query: '{ __typename }' }),
302
311
  });
303
312
  ressert(res).status.toBeBetween(400, 499);
304
313
  }),
@@ -503,6 +512,7 @@
503
512
  },
504
513
  });
505
514
  ressert(res).status.toBe(200);
515
+ await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
506
516
  }),
507
517
  audit('6A70', 'MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json', async () => {
508
518
  const url = new URL(await getUrl(opts.url));
@@ -563,21 +573,38 @@
563
573
  ressert(res).status.toBe(200);
564
574
  await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
565
575
  }),
566
- audit('B6DC', 'MAY use 4xx or 5xx status codes on JSON parsing failure', async () => {
576
+ audit('B6DC', 'MAY use 2xx, 4xx, or 5xx status codes on JSON parsing failure when accepting application/json', async () => {
567
577
  const res = await fetchFn(await getUrl(opts.url), {
568
578
  method: 'POST',
569
579
  headers: {
570
580
  'content-type': 'application/json',
581
+ accept: 'application/json',
571
582
  },
572
583
  body: '{ "not a JSON',
573
584
  });
574
- ressert(res).status.toBeBetween(400, 499);
585
+ ressert(res).status.toBeBetweenMultiple([
586
+ [200, 299],
587
+ [400, 499],
588
+ [500, 599],
589
+ ]);
575
590
  }),
576
- audit('BCF8', 'MAY use 400 status code on JSON parsing failure', async () => {
591
+ audit('BCF8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/json', async () => {
577
592
  const res = await fetchFn(await getUrl(opts.url), {
578
593
  method: 'POST',
579
594
  headers: {
580
595
  'content-type': 'application/json',
596
+ accept: 'application/json',
597
+ },
598
+ body: '{ "not a JSON',
599
+ });
600
+ ressert(res).status.toBe(400);
601
+ }),
602
+ audit('B7N8', 'SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
603
+ const res = await fetchFn(await getUrl(opts.url), {
604
+ method: 'POST',
605
+ headers: {
606
+ 'content-type': 'application/json',
607
+ accept: 'application/graphql-response+json',
581
608
  },
582
609
  body: '{ "not a JSON',
583
610
  });
@@ -867,10 +894,10 @@
867
894
  const headers = {};
868
895
  for (const [key, val] of res.headers.entries()) {
869
896
  // some headers change on each run, dont report it
870
- if (key === 'date') {
897
+ if (['date', 'expires'].includes(key)) {
871
898
  headers[key] = '<timestamp>';
872
899
  }
873
- else if (['cf-ray', 'server-timing', 'set-cookie'].includes(key)) {
900
+ else if (['cf-ray', 'server-timing', 'set-cookie', 'age'].includes(key)) {
874
901
  headers[key] = '<omitted>';
875
902
  }
876
903
  else {
@@ -1 +1 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).graphqlHttpAudits={})}(this,(function(e){"use strict";function t(e){return null===e?"null":Array.isArray(e)?"array":typeof e}function a(e,t,a){return{id:e,name:t,fn:async()=>{try{return await a(),{id:e,name:t,status:"ok"}}catch(a){if(!(a instanceof n))throw a;return{id:e,name:t,status:t.startsWith("MUST")?"error":t.startsWith("SHOULD")?"warn":"notice",reason:a.reason,response:a.response}}}}}class n{constructor(e,t){this.response=e,this.reason=t}}function o(e){return{status:{toBe(t){if(e.status!==t)throw new n(e,`Response status code is not ${t}`)},toBeBetween:(t,a)=>{if(!(t<=e.status&&e.status<=a))throw new n(e,`Response status is not between ${t} and ${a}`)}},header:t=>({toContain(a){var o;if(!(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a)))throw new n(e,`Response header ${t} does not contain ${a}`)},notToContain(a){var o;if(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a))throw new n(e,`Response header ${t} contains ${a}`)}}),bodyAsExecutionResult:{data:{async toBe(t){const a=e.clone();if((await s(e)).data!==t)throw new n(a,`Response body execution result data is not "${t}"`)}},async toHaveProperty(t){const a=e.clone();if(!(t in await s(e)))throw new n(a,`Response body execution result does not have a property "${t}"`)},async notToHaveProperty(t){const a=e.clone();if(t in await s(e))throw new n(a,`Response body execution result has a property "${t}"`)}}}}async function s(e){let t,a;try{const a=new TextDecoder("utf-8"),n=await e.arrayBuffer();t=a.decode(n)}catch(t){throw new n(e,"Response body is not UTF-8 encoded")}try{a=JSON.parse(t)}catch(t){throw new n(e,"Response body is not valid JSON")}return a}function i(e){const s=e.fetchFn||fetch;return[a("22EB","SHOULD accept application/graphql-response+json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/graphql-response+json")})),a("4655","MUST accept application/json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("47DE","SHOULD accept */* and use application/json for the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"*/*"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("80D8","SHOULD assume application/json content-type when accept is missing",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("82A3","MUST use utf-8 encoding when responding",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200);try{new TextDecoder("utf-8").decode(await t.arrayBuffer())}catch(e){throw new n(t,"Response body is not UTF-8 encoded")}})),a("BF61","MUST accept utf-8 encoded request",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json; charset=utf-8"},body:JSON.stringify({query:'{ __type(name: "Run🏃Swim🏊") { name } }'})})).status.toBe(200)})),a("78D5","MUST assume utf-8 in request if encoding is unspecified",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("2C94","MUST accept POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("5A70","MAY accept application/x-www-form-urlencoded formatted GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","{ __typename }");o(await s(t.toString())).status.toBe(200)})),a("9C48","MAY NOT allow executing mutations on GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","mutation { __typename }");o(await s(t.toString(),{headers:{accept:"application/graphql-response+json"}})).status.toBeBetween(400,499)})),a("9ABE","MAY respond with 4xx status code if content-type is not supplied on POST requests",(async()=>{o(await s(await r(e.url),{method:"POST"})).status.toBeBetween(400,499)})),a("03D4","MUST accept application/json POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("A5BF","MAY use 400 status code when request body is missing on POST",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"}})).status.toBe(400)})),a("423L","MAY use 400 status code on missing {query} parameter",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({notquery:"{ __typename }"})})).status.toBe(400)})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`LKJ${i}`,`MAY use 400 status code on ${t(n)} {query} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:n})})).status.toBe(400)})))),a("34A2","SHOULD allow string {query} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("13EE","MUST allow string {query} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`6C0${i}`,`MAY use 400 status code on ${t(n)} {operationName} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({operationName:n,query:"{ __typename }"})})).status.toBe(400)})))),a("8161","SHOULD allow string {operationName} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})})).status.toBe(200)})),a("B8B3","MUST allow string {operationName} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...["variables","operationName","extensions"].flatMap(((t,n)=>[a(`94B${n}`,`SHOULD allow null {${t}} parameter when accepting application/graphql-response+json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),a(`022${n}`,`MUST allow null {${t}} parameter when accepting application/json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")}))])),...["string",0,!1,["array"]].map(((n,i)=>a(`476${i}`,`MAY use 400 status code on ${t(n)} {variables} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",variables:n})})).status.toBe(400)})))),a("2EA1","SHOULD allow map {variables} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})})).status.toBe(200)})),a("28B9","MUST allow map {variables} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("D6D5","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));o(await s(t.toString(),{method:"GET",headers:{accept:"application/graphql-response+json"}})).status.toBe(200)})),a("6A70","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));const a=await s(t.toString(),{method:"GET",headers:{accept:"application/json"}});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),...["string",0,!1,["array"]].map(((n,i)=>a(`58B${i}`,`MAY use 400 status code on ${t(n)} {extensions} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:n})})).status.toBe(400)})))),a("428F","SHOULD allow map {extensions} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})})).status.toBe(200)})),a("1B7A","MUST allow map {extensions} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("B6DC","MAY use 4xx or 5xx status codes on JSON parsing failure",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:'{ "not a JSON'})).status.toBeBetween(400,499)})),a("BCF8","MAY use 400 status code on JSON parsing failure",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:'{ "not a JSON'})).status.toBe(400)})),a("8764","MAY use 4xx or 5xx status codes if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBeBetween(400,599)})),a("3E3A","MAY use 400 status code if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBe(400)})),a("572B","SHOULD use 200 status code on document parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{"})})).status.toBe(200)})),a("FDE2","SHOULD use 200 status code on document validation failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(200)})),a("7B9B","SHOULD use a status code of 200 on variable coercion failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(200)})),a("865D","SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBeBetween(400,599)})),a("556A","SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBe(400)})),a("D586","SHOULD not contain the data entry on document parsing failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("51FE","SHOULD use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBeBetween(400,599)})),a("74FF","SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(400)})),a("5E5B","SHOULD not contain the data entry on document validation failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("86EE","SHOULD use a status code of 400 on variable coercion failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(400)}))]}async function r(e){return"function"==typeof e?await e():e}async function p(e){var t;let a="";a+=`<li><code>${e.id}</code> ${e.name}\n`,a+="<details>\n",a+=`<summary>${function(e,t=1024){if(e.length>t)return e.substring(0,t)+"...";return e}(e.reason)}</summary>\n`,a+='<pre><code class="lang-json">';const n=e.response,o={};for(const[e,t]of n.headers.entries())"date"===e?o[e]="<timestamp>":["cf-ray","server-timing","set-cookie"].includes(e)?o[e]="<omitted>":o[e]=t;let s,i="";try{i=await n.text(),s=JSON.parse(i)}catch(e){(null===(t=n.headers.get("content-type"))||void 0===t?void 0:t.includes("text/html"))&&(i="<html omitted>")}return a+=JSON.stringify({status:n.status,statusText:n.statusText,headers:o,body:s||((null==i?void 0:i.length)>5120?"<body is too long>":i)||null},((e,t)=>{if(null!=t&&"object"==typeof t&&!Array.isArray(t)){const e={};return Object.keys(t).sort().reverse().reduce(((e,a)=>(e[a]=t[a],e)),e)}return t}),2)+"\n",a+="</code></pre>\n",a+="</details>\n",a+="</li>\n",a}e.auditServer=async function(e){const t=i(e);return await Promise.all(t.map((({fn:e})=>e())))},e.renderAuditResultsToHTML=async function(e){const t={total:0,ok:[],notice:[],warn:[],error:[]};for(const a of e)t.total++,a.status,t[a.status].push(a);let a="<i>* This report was auto-generated by graphql-http</i>\n";if(a+="\n",a+="<h1>GraphQL over HTTP audit report</h1>\n",a+="\n",a+="<ul>\n",a+=`<li><b>${t.total}</b> audits in total</li>\n`,t.ok.length&&(a+=`<li><span style="font-family: monospace">✅</span> <b>${t.ok.length}</b> pass</li>\n`),t.notice.length&&(a+=`<li><span style="font-family: monospace">💡</span> <b>${t.notice.length}</b> notices (suggestions)</li>\n`),t.warn.length&&(a+=`<li><span style="font-family: monospace">❗️</span> <b>${t.warn.length}</b> warnings (optional)</li>\n`),t.error.length&&(a+=`<li><span style="font-family: monospace">❌</span> <b>${t.error.length}</b> errors (required)</li>\n`),a+="</ul>\n",a+="\n",t.ok.length){a+="<h2>Passing</h2>\n",a+="<ol>\n";for(const[,e]of t.ok.entries())a+=`<li><code>${e.id}</code> ${e.name}</li>\n`;a+="</ol>\n",a+="\n"}if(t.notice.length){a+="<h2>Notices</h2>\n",a+="The server <i>MAY</i> support these, but are truly optional. These are suggestions following recommended conventions.\n",a+="<ol>\n";for(const[,e]of t.notice.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.warn.length){a+="<h2>Warnings</h2>\n",a+="The server <i>SHOULD</i> support these, but is not required.\n",a+="<ol>\n";for(const[,e]of t.warn.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.error.length){a+="<h2>Errors</h2>\n",a+="The server <b>MUST</b> support these.\n",a+="<ol>\n";for(const[,e]of t.error.entries())a+=await p(e);a+="</ol>\n"}return a},e.serverAudits=i}));
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).graphqlHttpAudits={})}(this,(function(e){"use strict";function t(e){return null===e?"null":Array.isArray(e)?"array":typeof e}function a(e,t,a){return{id:e,name:t,fn:async()=>{try{return await a(),{id:e,name:t,status:"ok"}}catch(a){if(!(a instanceof n))throw a;return{id:e,name:t,status:t.startsWith("MUST")?"error":t.startsWith("SHOULD")?"warn":"notice",reason:a.reason,response:a.response}}}}}class n{constructor(e,t){this.response=e,this.reason=t}}function o(e){return{status:{toBe(t){if(e.status!==t)throw new n(e,`Response status code is not ${t}`)},toBeBetween:(t,a)=>{if(!(t<=e.status&&e.status<=a))throw new n(e,`Response status is not between ${t} and ${a}`)},toBeBetweenMultiple:t=>{if(!t.some((([t,a])=>t<=e.status&&e.status<=a)))throw new n(e,`Response status is not between any of the provided ranges: ${t.map((([e,t])=>`[${e}, ${t}]`)).join(", ")}`)}},header:t=>({toContain(a){var o;if(!(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a)))throw new n(e,`Response header ${t} does not contain ${a}`)},notToContain(a){var o;if(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a))throw new n(e,`Response header ${t} contains ${a}`)}}),bodyAsExecutionResult:{data:{async toBe(t){const a=e.clone();if((await s(e)).data!==t)throw new n(a,`Response body execution result data is not "${t}"`)}},async toHaveProperty(t){const a=e.clone();if(!(t in await s(e)))throw new n(a,`Response body execution result does not have a property "${t}"`)},async notToHaveProperty(t){const a=e.clone();if(t in await s(e))throw new n(a,`Response body execution result has a property "${t}"`)}}}}async function s(e){let t,a;try{const a=new TextDecoder("utf-8"),n=await e.arrayBuffer();t=a.decode(n)}catch(t){throw new n(e,"Response body is not UTF-8 encoded")}try{a=JSON.parse(t)}catch(t){throw new n(e,"Response body is not valid JSON")}return a}function i(e){const s=e.fetchFn||fetch;return[a("22EB","SHOULD accept application/graphql-response+json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/graphql-response+json")})),a("4655","MUST accept application/json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("47DE","SHOULD accept */* and use application/json for the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"*/*"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("80D8","SHOULD assume application/json content-type when accept is missing",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("82A3","MUST use utf-8 encoding when responding",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200);try{new TextDecoder("utf-8").decode(await t.arrayBuffer())}catch(e){throw new n(t,"Response body is not UTF-8 encoded")}})),a("BF61","MUST accept utf-8 encoded request",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json; charset=utf-8"},body:JSON.stringify({query:'{ __type(name: "Run🏃Swim🏊") { name } }'})})).status.toBe(200)})),a("78D5","MUST assume utf-8 in request if encoding is unspecified",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("2C94","MUST accept POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("5A70","MAY accept application/x-www-form-urlencoded formatted GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","{ __typename }");o(await s(t.toString())).status.toBe(200)})),a("9C48","MAY NOT allow executing mutations on GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","mutation { __typename }");o(await s(t.toString(),{headers:{accept:"application/graphql-response+json"}})).status.toBeBetween(400,499)})),a("9ABE","SHOULD respond with 4xx status code if content-type is not supplied on POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",body:JSON.stringify({query:"{ __typename }"})})).status.toBeBetween(400,499)})),a("03D4","MUST accept application/json POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("A5BF","MAY use 400 status code when request body is missing on POST",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"}})).status.toBe(400)})),a("423L","MAY use 400 status code on missing {query} parameter",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({notquery:"{ __typename }"})})).status.toBe(400)})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`LKJ${i}`,`MAY use 400 status code on ${t(n)} {query} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:n})})).status.toBe(400)})))),a("34A2","SHOULD allow string {query} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("13EE","MUST allow string {query} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`6C0${i}`,`MAY use 400 status code on ${t(n)} {operationName} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({operationName:n,query:"{ __typename }"})})).status.toBe(400)})))),a("8161","SHOULD allow string {operationName} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})})).status.toBe(200)})),a("B8B3","MUST allow string {operationName} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...["variables","operationName","extensions"].flatMap(((t,n)=>[a(`94B${n}`,`SHOULD allow null {${t}} parameter when accepting application/graphql-response+json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),a(`022${n}`,`MUST allow null {${t}} parameter when accepting application/json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")}))])),...["string",0,!1,["array"]].map(((n,i)=>a(`476${i}`,`MAY use 400 status code on ${t(n)} {variables} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",variables:n})})).status.toBe(400)})))),a("2EA1","SHOULD allow map {variables} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})})).status.toBe(200)})),a("28B9","MUST allow map {variables} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("D6D5","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));const a=await s(t.toString(),{method:"GET",headers:{accept:"application/graphql-response+json"}});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),a("6A70","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));const a=await s(t.toString(),{method:"GET",headers:{accept:"application/json"}});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),...["string",0,!1,["array"]].map(((n,i)=>a(`58B${i}`,`MAY use 400 status code on ${t(n)} {extensions} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:n})})).status.toBe(400)})))),a("428F","SHOULD allow map {extensions} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})})).status.toBe(200)})),a("1B7A","MUST allow map {extensions} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("B6DC","MAY use 2xx, 4xx, or 5xx status codes on JSON parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:'{ "not a JSON'})).status.toBeBetweenMultiple([[200,299],[400,499],[500,599]])})),a("BCF8","SHOULD use 400 status code on JSON parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:'{ "not a JSON'})).status.toBe(400)})),a("B7N8","SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:'{ "not a JSON'})).status.toBe(400)})),a("8764","MAY use 4xx or 5xx status codes if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBeBetween(400,599)})),a("3E3A","MAY use 400 status code if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBe(400)})),a("572B","SHOULD use 200 status code on document parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{"})})).status.toBe(200)})),a("FDE2","SHOULD use 200 status code on document validation failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(200)})),a("7B9B","SHOULD use a status code of 200 on variable coercion failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(200)})),a("865D","SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBeBetween(400,599)})),a("556A","SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBe(400)})),a("D586","SHOULD not contain the data entry on document parsing failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("51FE","SHOULD use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBeBetween(400,599)})),a("74FF","SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(400)})),a("5E5B","SHOULD not contain the data entry on document validation failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("86EE","SHOULD use a status code of 400 on variable coercion failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(400)}))]}async function r(e){return"function"==typeof e?await e():e}async function p(e){var t;let a="";a+=`<li><code>${e.id}</code> ${e.name}\n`,a+="<details>\n",a+=`<summary>${function(e,t=1024){if(e.length>t)return e.substring(0,t)+"...";return e}(e.reason)}</summary>\n`,a+='<pre><code class="lang-json">';const n=e.response,o={};for(const[e,t]of n.headers.entries())["date","expires"].includes(e)?o[e]="<timestamp>":["cf-ray","server-timing","set-cookie","age"].includes(e)?o[e]="<omitted>":o[e]=t;let s,i="";try{i=await n.text(),s=JSON.parse(i)}catch(e){(null===(t=n.headers.get("content-type"))||void 0===t?void 0:t.includes("text/html"))&&(i="<html omitted>")}return a+=JSON.stringify({status:n.status,statusText:n.statusText,headers:o,body:s||((null==i?void 0:i.length)>5120?"<body is too long>":i)||null},((e,t)=>{if(null!=t&&"object"==typeof t&&!Array.isArray(t)){const e={};return Object.keys(t).sort().reverse().reduce(((e,a)=>(e[a]=t[a],e)),e)}return t}),2)+"\n",a+="</code></pre>\n",a+="</details>\n",a+="</li>\n",a}e.auditServer=async function(e){const t=i(e);return await Promise.all(t.map((({fn:e})=>e())))},e.renderAuditResultsToHTML=async function(e){const t={total:0,ok:[],notice:[],warn:[],error:[]};for(const a of e)t.total++,a.status,t[a.status].push(a);let a="<i>* This report was auto-generated by graphql-http</i>\n";if(a+="\n",a+="<h1>GraphQL over HTTP audit report</h1>\n",a+="\n",a+="<ul>\n",a+=`<li><b>${t.total}</b> audits in total</li>\n`,t.ok.length&&(a+=`<li><span style="font-family: monospace">✅</span> <b>${t.ok.length}</b> pass</li>\n`),t.notice.length&&(a+=`<li><span style="font-family: monospace">💡</span> <b>${t.notice.length}</b> notices (suggestions)</li>\n`),t.warn.length&&(a+=`<li><span style="font-family: monospace">❗️</span> <b>${t.warn.length}</b> warnings (optional)</li>\n`),t.error.length&&(a+=`<li><span style="font-family: monospace">❌</span> <b>${t.error.length}</b> errors (required)</li>\n`),a+="</ul>\n",a+="\n",t.ok.length){a+="<h2>Passing</h2>\n",a+="<ol>\n";for(const[,e]of t.ok.entries())a+=`<li><code>${e.id}</code> ${e.name}</li>\n`;a+="</ol>\n",a+="\n"}if(t.notice.length){a+="<h2>Notices</h2>\n",a+="The server <i>MAY</i> support these, but are truly optional. These are suggestions following recommended conventions.\n",a+="<ol>\n";for(const[,e]of t.notice.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.warn.length){a+="<h2>Warnings</h2>\n",a+="The server <i>SHOULD</i> support these, but is not required.\n",a+="<ol>\n";for(const[,e]of t.warn.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.error.length){a+="<h2>Errors</h2>\n",a+="The server <b>MUST</b> support these.\n",a+="<ol>\n";for(const[,e]of t.error.entries())a+=await p(e);a+="</ol>\n"}return a},e.serverAudits=i}));
Binary file