graphql-http 1.21.0 → 1.22.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
@@ -264,6 +264,16 @@ const client = createClient({
264
264
  })();
265
265
  ```
266
266
 
267
+ #### Serve [GraphiQL](https://github.com/graphql/graphiql)
268
+
269
+ Thanks to [`ruru`](https://github.com/graphile/crystal/tree/main/grafast/ruru), serving GraphiQL is as easy as running:
270
+
271
+ ```sh
272
+ npx ruru -SP -p 4001 -e http://localhost:4000/graphql
273
+ ```
274
+
275
+ Open [http://localhost:4001](http://localhost:4001) in the browser to use it.
276
+
267
277
  ## Recipes
268
278
 
269
279
  <details id="promise">
@@ -497,7 +507,7 @@ const client = createClient({
497
507
  <summary><a href="#browser">🔗</a> Client usage in browser</summary>
498
508
 
499
509
  ```html
500
- <!DOCTYPE html>
510
+ <!doctype html>
501
511
  <html>
502
512
  <head>
503
513
  <meta charset="utf-8" />
@@ -89,6 +89,7 @@ async function renderAuditResultsToHTML(results) {
89
89
  }
90
90
  exports.renderAuditResultsToHTML = renderAuditResultsToHTML;
91
91
  async function printAuditFail(result) {
92
+ var _a;
92
93
  let report = '';
93
94
  report += `<li><code>${result.id}</code> ${result.name}\n`;
94
95
  report += '<details>\n';
@@ -101,7 +102,7 @@ async function printAuditFail(result) {
101
102
  if (key === 'date') {
102
103
  headers[key] = '<timestamp>';
103
104
  }
104
- else if (['cf-ray', 'server-timing'].includes(key)) {
105
+ else if (['cf-ray', 'server-timing', 'set-cookie'].includes(key)) {
105
106
  headers[key] = '<omitted>';
106
107
  }
107
108
  else {
@@ -112,9 +113,13 @@ async function printAuditFail(result) {
112
113
  try {
113
114
  text = await res.text();
114
115
  json = JSON.parse(text);
116
+ // is json, there shouldnt be nothing to sanitize (hopefully)
115
117
  }
116
- catch (_a) {
117
- // noop
118
+ catch (_b) {
119
+ // is not json, avoid rendering html (rest is allowed)
120
+ if ((_a = res.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.includes('text/html')) {
121
+ text = '<html omitted>';
122
+ }
118
123
  }
119
124
  const stringified = JSON.stringify({
120
125
  status: res.status,
@@ -85,6 +85,7 @@ export async function renderAuditResultsToHTML(results) {
85
85
  return report;
86
86
  }
87
87
  async function printAuditFail(result) {
88
+ var _a;
88
89
  let report = '';
89
90
  report += `<li><code>${result.id}</code> ${result.name}\n`;
90
91
  report += '<details>\n';
@@ -97,7 +98,7 @@ async function printAuditFail(result) {
97
98
  if (key === 'date') {
98
99
  headers[key] = '<timestamp>';
99
100
  }
100
- else if (['cf-ray', 'server-timing'].includes(key)) {
101
+ else if (['cf-ray', 'server-timing', 'set-cookie'].includes(key)) {
101
102
  headers[key] = '<omitted>';
102
103
  }
103
104
  else {
@@ -108,9 +109,13 @@ async function printAuditFail(result) {
108
109
  try {
109
110
  text = await res.text();
110
111
  json = JSON.parse(text);
112
+ // is json, there shouldnt be nothing to sanitize (hopefully)
111
113
  }
112
- catch (_a) {
113
- // noop
114
+ catch (_b) {
115
+ // is not json, avoid rendering html (rest is allowed)
116
+ if ((_a = res.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.includes('text/html')) {
117
+ text = '<html omitted>';
118
+ }
114
119
  }
115
120
  const stringified = JSON.stringify({
116
121
  status: res.status,
package/lib/handler.d.mts CHANGED
@@ -102,8 +102,10 @@ export type OperationContext = Record<PropertyKey, unknown> | symbol | number |
102
102
  */
103
103
  export type FormatError = (err: Readonly<GraphQLError | Error>) => GraphQLError | Error;
104
104
  /**
105
- * The request parser for an incoming GraphQL request. It parses and validates the
106
- * request itself, including the request method and the content-type of the body.
105
+ * The request parser for an incoming GraphQL request in the handler.
106
+ *
107
+ * It should parse and validate the request itself, including the request method
108
+ * and the content-type of the body.
107
109
  *
108
110
  * In case you are extending the server to handle more request types, this is the
109
111
  * perfect place to do so.
@@ -120,6 +122,21 @@ export type FormatError = (err: Readonly<GraphQLError | Error>) => GraphQLError
120
122
  * @category Server
121
123
  */
122
124
  export type ParseRequestParams<RequestRaw = unknown, RequestContext = unknown> = (req: Request<RequestRaw, RequestContext>) => Promise<RequestParams | Response | void> | RequestParams | Response | void;
125
+ /**
126
+ * The GraphQL over HTTP spec compliant request parser for an incoming GraphQL request.
127
+ * It parses and validates the request itself, including the request method and the
128
+ * content-type of the body.
129
+ *
130
+ * If the HTTP request itself is invalid or malformed, the function will return an
131
+ * appropriate {@link Response}.
132
+ *
133
+ * If the HTTP request is valid, but is not a well-formatted GraphQL request, the
134
+ * function will throw an error and it is up to the user to handle and respond as
135
+ * they see fit.
136
+ *
137
+ * @category Server
138
+ */
139
+ export declare function parseRequestParams<RequestRaw = unknown, RequestContext = unknown>(req: Request<RequestRaw, RequestContext>): Promise<Response | RequestParams>;
123
140
  /** @category Server */
124
141
  export type OperationArgs<Context extends OperationContext = undefined> = ExecutionArgs & {
125
142
  contextValue?: Context;
package/lib/handler.d.ts CHANGED
@@ -102,8 +102,10 @@ export type OperationContext = Record<PropertyKey, unknown> | symbol | number |
102
102
  */
103
103
  export type FormatError = (err: Readonly<GraphQLError | Error>) => GraphQLError | Error;
104
104
  /**
105
- * The request parser for an incoming GraphQL request. It parses and validates the
106
- * request itself, including the request method and the content-type of the body.
105
+ * The request parser for an incoming GraphQL request in the handler.
106
+ *
107
+ * It should parse and validate the request itself, including the request method
108
+ * and the content-type of the body.
107
109
  *
108
110
  * In case you are extending the server to handle more request types, this is the
109
111
  * perfect place to do so.
@@ -120,6 +122,21 @@ export type FormatError = (err: Readonly<GraphQLError | Error>) => GraphQLError
120
122
  * @category Server
121
123
  */
122
124
  export type ParseRequestParams<RequestRaw = unknown, RequestContext = unknown> = (req: Request<RequestRaw, RequestContext>) => Promise<RequestParams | Response | void> | RequestParams | Response | void;
125
+ /**
126
+ * The GraphQL over HTTP spec compliant request parser for an incoming GraphQL request.
127
+ * It parses and validates the request itself, including the request method and the
128
+ * content-type of the body.
129
+ *
130
+ * If the HTTP request itself is invalid or malformed, the function will return an
131
+ * appropriate {@link Response}.
132
+ *
133
+ * If the HTTP request is valid, but is not a well-formatted GraphQL request, the
134
+ * function will throw an error and it is up to the user to handle and respond as
135
+ * they see fit.
136
+ *
137
+ * @category Server
138
+ */
139
+ export declare function parseRequestParams<RequestRaw = unknown, RequestContext = unknown>(req: Request<RequestRaw, RequestContext>): Promise<Response | RequestParams>;
123
140
  /** @category Server */
124
141
  export type OperationArgs<Context extends OperationContext = undefined> = ExecutionArgs & {
125
142
  contextValue?: Context;
package/lib/handler.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.createHandler = void 0;
8
+ exports.createHandler = exports.parseRequestParams = void 0;
9
9
  const graphql_1 = require("graphql");
10
10
  const utils_1 = require("./utils");
11
11
  /** Checks whether the passed value is the `graphql-http` server agnostic response. */
@@ -15,6 +15,117 @@ function isResponse(val) {
15
15
  (typeof val[0] === 'string' || val[0] === null) &&
16
16
  (0, utils_1.isObject)(val[1]));
17
17
  }
18
+ /**
19
+ * The GraphQL over HTTP spec compliant request parser for an incoming GraphQL request.
20
+ * It parses and validates the request itself, including the request method and the
21
+ * content-type of the body.
22
+ *
23
+ * If the HTTP request itself is invalid or malformed, the function will return an
24
+ * appropriate {@link Response}.
25
+ *
26
+ * If the HTTP request is valid, but is not a well-formatted GraphQL request, the
27
+ * function will throw an error and it is up to the user to handle and respond as
28
+ * they see fit.
29
+ *
30
+ * @category Server
31
+ */
32
+ async function parseRequestParams(req) {
33
+ var _a, _b;
34
+ const method = req.method;
35
+ if (method !== 'GET' && method !== 'POST') {
36
+ return [
37
+ null,
38
+ {
39
+ status: 405,
40
+ statusText: 'Method Not Allowed',
41
+ headers: {
42
+ allow: 'GET, POST',
43
+ },
44
+ },
45
+ ];
46
+ }
47
+ const [mediaType, charset = 'charset=utf-8', // utf-8 is assumed when not specified. this parameter is either "charset" or "boundary" (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length)
48
+ ] = (getHeader(req, 'content-type') || '')
49
+ .replace(/\s/g, '')
50
+ .toLowerCase()
51
+ .split(';');
52
+ const partParams = {};
53
+ switch (true) {
54
+ case method === 'GET': {
55
+ // TODO: what if content-type is specified and is not application/x-www-form-urlencoded?
56
+ try {
57
+ const [, search] = req.url.split('?');
58
+ const searchParams = new URLSearchParams(search);
59
+ partParams.operationName =
60
+ (_a = searchParams.get('operationName')) !== null && _a !== void 0 ? _a : undefined;
61
+ partParams.query = (_b = searchParams.get('query')) !== null && _b !== void 0 ? _b : undefined;
62
+ const variables = searchParams.get('variables');
63
+ if (variables)
64
+ partParams.variables = JSON.parse(variables);
65
+ const extensions = searchParams.get('extensions');
66
+ if (extensions)
67
+ partParams.extensions = JSON.parse(extensions);
68
+ }
69
+ catch (_c) {
70
+ throw new Error('Unparsable URL');
71
+ }
72
+ break;
73
+ }
74
+ case method === 'POST' &&
75
+ mediaType === 'application/json' &&
76
+ charset === 'charset=utf-8':
77
+ {
78
+ if (!req.body) {
79
+ throw new Error('Missing body');
80
+ }
81
+ let data;
82
+ try {
83
+ const body = typeof req.body === 'function' ? await req.body() : req.body;
84
+ data = typeof body === 'string' ? JSON.parse(body) : body;
85
+ }
86
+ catch (err) {
87
+ throw new Error('Unparsable JSON body');
88
+ }
89
+ if (!(0, utils_1.isObject)(data)) {
90
+ throw new Error('JSON body must be an object');
91
+ }
92
+ partParams.operationName = data.operationName;
93
+ partParams.query = data.query;
94
+ partParams.variables = data.variables;
95
+ partParams.extensions = data.extensions;
96
+ break;
97
+ }
98
+ default: // graphql-http doesnt support any other content type
99
+ return [
100
+ null,
101
+ {
102
+ status: 415,
103
+ statusText: 'Unsupported Media Type',
104
+ },
105
+ ];
106
+ }
107
+ if (partParams.query == null)
108
+ throw new Error('Missing query');
109
+ if (typeof partParams.query !== 'string')
110
+ throw new Error('Invalid query');
111
+ if (partParams.variables != null &&
112
+ (typeof partParams.variables !== 'object' ||
113
+ Array.isArray(partParams.variables))) {
114
+ throw new Error('Invalid variables');
115
+ }
116
+ if (partParams.operationName != null &&
117
+ typeof partParams.operationName !== 'string') {
118
+ throw new Error('Invalid operationName');
119
+ }
120
+ if (partParams.extensions != null &&
121
+ (typeof partParams.extensions !== 'object' ||
122
+ Array.isArray(partParams.extensions))) {
123
+ throw new Error('Invalid extensions');
124
+ }
125
+ // request parameters are checked and now complete
126
+ return partParams;
127
+ }
128
+ exports.parseRequestParams = parseRequestParams;
18
129
  /**
19
130
  * Makes a GraphQL over HTTP spec compliant server handler. The handler can
20
131
  * be used with your favorite server library.
@@ -71,7 +182,7 @@ function isResponse(val) {
71
182
  * @category Server
72
183
  */
73
184
  function createHandler(options) {
74
- 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, parseRequestParams = defaultParseRequestParams, } = options;
185
+ 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, parseRequestParams: optionsParseRequestParams = parseRequestParams, } = options;
75
186
  return async function handler(req) {
76
187
  let acceptedMediaType = null;
77
188
  const accepts = (getHeader(req, 'accept') || '*/*')
@@ -82,9 +193,9 @@ function createHandler(options) {
82
193
  // accept-charset became obsolete, shouldnt be used (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset)
83
194
  // TODO: handle the weight parameter "q"
84
195
  const [mediaType, ...params] = accept.split(';');
85
- const charset = (params === null || params === void 0 ? void 0 : params.find((param) => param.includes('charset='))) || 'charset=utf8'; // utf-8 is assumed when not specified;
196
+ const charset = (params === null || params === void 0 ? void 0 : params.find((param) => param.includes('charset='))) || 'charset=utf-8'; // utf-8 is assumed when not specified;
86
197
  if (mediaType === 'application/graphql-response+json' &&
87
- charset === 'charset=utf8') {
198
+ charset === 'charset=utf-8') {
88
199
  acceptedMediaType = 'application/graphql-response+json';
89
200
  break;
90
201
  }
@@ -92,7 +203,7 @@ function createHandler(options) {
92
203
  if ((mediaType === 'application/json' ||
93
204
  mediaType === 'application/*' ||
94
205
  mediaType === '*/*') &&
95
- charset === 'charset=utf8') {
206
+ (charset === 'charset=utf-8' || charset === 'charset=utf8')) {
96
207
  acceptedMediaType = 'application/json';
97
208
  break;
98
209
  }
@@ -111,9 +222,9 @@ function createHandler(options) {
111
222
  }
112
223
  let params;
113
224
  try {
114
- let paramsOrRes = await parseRequestParams(req);
225
+ let paramsOrRes = await optionsParseRequestParams(req);
115
226
  if (!paramsOrRes)
116
- paramsOrRes = await defaultParseRequestParams(req);
227
+ paramsOrRes = await parseRequestParams(req);
117
228
  if (isResponse(paramsOrRes))
118
229
  return paramsOrRes;
119
230
  params = paramsOrRes;
@@ -222,110 +333,6 @@ function createHandler(options) {
222
333
  };
223
334
  }
224
335
  exports.createHandler = createHandler;
225
- /**
226
- * The default request params parser. Used when no custom one is provided or if it
227
- * returns nothing.
228
- *
229
- * Read more about it in {@link ParseRequestParams}.
230
- *
231
- * TODO: should graphql-http itself care about content-encoding? I'd say unzipping should happen before handler is reached
232
- */
233
- async function defaultParseRequestParams(req) {
234
- var _a, _b;
235
- const method = req.method;
236
- if (method !== 'GET' && method !== 'POST') {
237
- return [
238
- null,
239
- {
240
- status: 405,
241
- statusText: 'Method Not Allowed',
242
- headers: {
243
- allow: 'GET, POST',
244
- },
245
- },
246
- ];
247
- }
248
- const [mediaType, charset = 'charset=utf-8', // utf-8 is assumed when not specified. this parameter is either "charset" or "boundary" (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length)
249
- ] = (getHeader(req, 'content-type') || '')
250
- .replace(/\s/g, '')
251
- .toLowerCase()
252
- .split(';');
253
- const partParams = {};
254
- switch (true) {
255
- case method === 'GET': {
256
- // TODO: what if content-type is specified and is not application/x-www-form-urlencoded?
257
- try {
258
- const [, search] = req.url.split('?');
259
- const searchParams = new URLSearchParams(search);
260
- partParams.operationName =
261
- (_a = searchParams.get('operationName')) !== null && _a !== void 0 ? _a : undefined;
262
- partParams.query = (_b = searchParams.get('query')) !== null && _b !== void 0 ? _b : undefined;
263
- const variables = searchParams.get('variables');
264
- if (variables)
265
- partParams.variables = JSON.parse(variables);
266
- const extensions = searchParams.get('extensions');
267
- if (extensions)
268
- partParams.extensions = JSON.parse(extensions);
269
- }
270
- catch (_c) {
271
- throw new Error('Unparsable URL');
272
- }
273
- break;
274
- }
275
- case method === 'POST' &&
276
- mediaType === 'application/json' &&
277
- charset === 'charset=utf-8':
278
- {
279
- if (!req.body) {
280
- throw new Error('Missing body');
281
- }
282
- let data;
283
- try {
284
- const body = typeof req.body === 'function' ? await req.body() : req.body;
285
- data = typeof body === 'string' ? JSON.parse(body) : body;
286
- }
287
- catch (err) {
288
- throw new Error('Unparsable JSON body');
289
- }
290
- if (!(0, utils_1.isObject)(data)) {
291
- throw new Error('JSON body must be an object');
292
- }
293
- partParams.operationName = data.operationName;
294
- partParams.query = data.query;
295
- partParams.variables = data.variables;
296
- partParams.extensions = data.extensions;
297
- break;
298
- }
299
- default: // graphql-http doesnt support any other content type
300
- return [
301
- null,
302
- {
303
- status: 415,
304
- statusText: 'Unsupported Media Type',
305
- },
306
- ];
307
- }
308
- if (partParams.query == null)
309
- throw new Error('Missing query');
310
- if (typeof partParams.query !== 'string')
311
- throw new Error('Invalid query');
312
- if (partParams.variables != null &&
313
- (typeof partParams.variables !== 'object' ||
314
- Array.isArray(partParams.variables))) {
315
- throw new Error('Invalid variables');
316
- }
317
- if (partParams.operationName != null &&
318
- typeof partParams.operationName !== 'string') {
319
- throw new Error('Invalid operationName');
320
- }
321
- if (partParams.extensions != null &&
322
- (typeof partParams.extensions !== 'object' ||
323
- Array.isArray(partParams.extensions))) {
324
- throw new Error('Invalid extensions');
325
- }
326
- // request parameters are checked and now complete
327
- return partParams;
328
- }
329
336
  /**
330
337
  * Creates an appropriate GraphQL over HTTP response following the provided arguments.
331
338
  *