graphql-http 1.19.0 → 1.21.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.
package/lib/handler.js CHANGED
@@ -5,24 +5,19 @@
5
5
  *
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.makeResponse = exports.getAcceptableMediaType = exports.createHandler = exports.isResponse = void 0;
8
+ exports.createHandler = void 0;
9
9
  const graphql_1 = require("graphql");
10
10
  const utils_1 = require("./utils");
11
- /**
12
- * Checks whether the passed value is the `graphql-http` server agnostic response.
13
- *
14
- * @category Server
15
- */
11
+ /** Checks whether the passed value is the `graphql-http` server agnostic response. */
16
12
  function isResponse(val) {
17
13
  // TODO: make sure the contents of init match ResponseInit
18
14
  return (Array.isArray(val) &&
19
15
  (typeof val[0] === 'string' || val[0] === null) &&
20
16
  (0, utils_1.isObject)(val[1]));
21
17
  }
22
- exports.isResponse = isResponse;
23
18
  /**
24
19
  * Makes a GraphQL over HTTP spec compliant server handler. The handler can
25
- * be used with your favourite server library.
20
+ * be used with your favorite server library.
26
21
  *
27
22
  * Beware that the handler resolves only after the whole operation completes.
28
23
  *
@@ -76,23 +71,32 @@ exports.isResponse = isResponse;
76
71
  * @category Server
77
72
  */
78
73
  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, formatError = (err) => err, } = 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;
80
75
  return async function handler(req) {
81
- var _a, _b;
82
- const method = req.method;
83
- if (method !== 'GET' && method !== 'POST') {
84
- return [
85
- null,
86
- {
87
- status: 405,
88
- statusText: 'Method Not Allowed',
89
- headers: {
90
- allow: 'GET, POST',
91
- },
92
- },
93
- ];
76
+ let acceptedMediaType = null;
77
+ const accepts = (getHeader(req, 'accept') || '*/*')
78
+ .replace(/\s/g, '')
79
+ .toLowerCase()
80
+ .split(',');
81
+ for (const accept of accepts) {
82
+ // accept-charset became obsolete, shouldnt be used (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset)
83
+ // TODO: handle the weight parameter "q"
84
+ 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;
86
+ if (mediaType === 'application/graphql-response+json' &&
87
+ charset === 'charset=utf8') {
88
+ acceptedMediaType = 'application/graphql-response+json';
89
+ break;
90
+ }
91
+ // application/json should be the default until watershed
92
+ if ((mediaType === 'application/json' ||
93
+ mediaType === 'application/*' ||
94
+ mediaType === '*/*') &&
95
+ charset === 'charset=utf8') {
96
+ acceptedMediaType = 'application/json';
97
+ break;
98
+ }
94
99
  }
95
- const acceptedMediaType = getAcceptableMediaType(getHeader(req, 'accept'));
96
100
  if (!acceptedMediaType) {
97
101
  return [
98
102
  null,
@@ -105,89 +109,14 @@ function createHandler(options) {
105
109
  },
106
110
  ];
107
111
  }
108
- // TODO: should graphql-http care about content-encoding? I'd say unzipping should happen before handler is reached
109
- 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)
110
- ] = (getHeader(req, 'content-type') || '')
111
- .replace(/\s/g, '')
112
- .toLowerCase()
113
- .split(';');
114
112
  let params;
115
113
  try {
116
- const partParams = {};
117
- switch (true) {
118
- case method === 'GET': {
119
- // TODO: what if content-type is specified and is not application/x-www-form-urlencoded?
120
- try {
121
- const [, search] = req.url.split('?');
122
- const searchParams = new URLSearchParams(search);
123
- partParams.operationName =
124
- (_a = searchParams.get('operationName')) !== null && _a !== void 0 ? _a : undefined;
125
- partParams.query = (_b = searchParams.get('query')) !== null && _b !== void 0 ? _b : undefined;
126
- const variables = searchParams.get('variables');
127
- if (variables)
128
- partParams.variables = JSON.parse(variables);
129
- const extensions = searchParams.get('extensions');
130
- if (extensions)
131
- partParams.extensions = JSON.parse(extensions);
132
- }
133
- catch (_c) {
134
- throw new Error('Unparsable URL');
135
- }
136
- break;
137
- }
138
- case method === 'POST' &&
139
- mediaType === 'application/json' &&
140
- charset === 'charset=utf-8':
141
- {
142
- if (!req.body) {
143
- throw new Error('Missing body');
144
- }
145
- let data;
146
- try {
147
- const body = typeof req.body === 'function' ? await req.body() : req.body;
148
- data = typeof body === 'string' ? JSON.parse(body) : body;
149
- }
150
- catch (err) {
151
- throw new Error('Unparsable JSON body');
152
- }
153
- if (!(0, utils_1.isObject)(data)) {
154
- throw new Error('JSON body must be an object');
155
- }
156
- partParams.operationName = data.operationName;
157
- partParams.query = data.query;
158
- partParams.variables = data.variables;
159
- partParams.extensions = data.extensions;
160
- break;
161
- }
162
- default: // graphql-http doesnt support any other content type
163
- return [
164
- null,
165
- {
166
- status: 415,
167
- statusText: 'Unsupported Media Type',
168
- },
169
- ];
170
- }
171
- if (partParams.query == null)
172
- throw new Error('Missing query');
173
- if (typeof partParams.query !== 'string')
174
- throw new Error('Invalid query');
175
- if (partParams.variables != null &&
176
- (typeof partParams.variables !== 'object' ||
177
- Array.isArray(partParams.variables))) {
178
- throw new Error('Invalid variables');
179
- }
180
- if (partParams.operationName != null &&
181
- typeof partParams.operationName !== 'string') {
182
- throw new Error('Invalid operationName');
183
- }
184
- if (partParams.extensions != null &&
185
- (typeof partParams.extensions !== 'object' ||
186
- Array.isArray(partParams.extensions))) {
187
- throw new Error('Invalid extensions');
188
- }
189
- // request parameters are checked and now complete
190
- params = partParams;
114
+ let paramsOrRes = await parseRequestParams(req);
115
+ if (!paramsOrRes)
116
+ paramsOrRes = await defaultParseRequestParams(req);
117
+ if (isResponse(paramsOrRes))
118
+ return paramsOrRes;
119
+ params = paramsOrRes;
191
120
  }
192
121
  catch (err) {
193
122
  return makeResponse(err, acceptedMediaType, formatError);
@@ -197,7 +126,7 @@ function createHandler(options) {
197
126
  if (isResponse(maybeResErrsOrArgs))
198
127
  return maybeResErrsOrArgs;
199
128
  else if ((0, utils_1.isExecutionResult)(maybeResErrsOrArgs) ||
200
- (0, utils_1.areGraphQLErrors)(maybeResErrsOrArgs))
129
+ areGraphQLErrors(maybeResErrsOrArgs))
201
130
  return makeResponse(maybeResErrsOrArgs, acceptedMediaType, formatError);
202
131
  else if (maybeResErrsOrArgs)
203
132
  args = maybeResErrsOrArgs;
@@ -249,7 +178,7 @@ function createHandler(options) {
249
178
  throw null;
250
179
  operation = ast.operation;
251
180
  }
252
- catch (_d) {
181
+ catch (_a) {
253
182
  return makeResponse(new graphql_1.GraphQLError('Unable to detect operation AST'), acceptedMediaType, formatError);
254
183
  }
255
184
  if (operation === 'subscription') {
@@ -257,7 +186,7 @@ function createHandler(options) {
257
186
  }
258
187
  // mutations cannot happen over GETs
259
188
  // https://graphql.github.io/graphql-over-http/draft/#sel-CALFJRPAAELBAAxwP
260
- if (operation === 'mutation' && method === 'GET') {
189
+ if (operation === 'mutation' && req.method === 'GET') {
261
190
  return [
262
191
  JSON.stringify({
263
192
  errors: [new graphql_1.GraphQLError('Cannot perform mutations over GET')],
@@ -294,38 +223,109 @@ function createHandler(options) {
294
223
  }
295
224
  exports.createHandler = createHandler;
296
225
  /**
297
- * Inspects the request and detects the appropriate/acceptable Media-Type
298
- * looking at the `Accept` header while complying with the GraphQL over HTTP spec.
226
+ * The default request params parser. Used when no custom one is provided or if it
227
+ * returns nothing.
299
228
  *
300
- * @category Server
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
301
232
  */
302
- function getAcceptableMediaType(acceptHeader) {
303
- let acceptedMediaType = null;
304
- const accepts = (acceptHeader || '*/*')
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') || '')
305
250
  .replace(/\s/g, '')
306
251
  .toLowerCase()
307
- .split(',');
308
- for (const accept of accepts) {
309
- // accept-charset became obsolete, shouldnt be used (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset)
310
- // TODO: handle the weight parameter "q"
311
- const [mediaType, ...params] = accept.split(';');
312
- const charset = (params === null || params === void 0 ? void 0 : params.find((param) => param.includes('charset='))) || 'charset=utf8'; // utf-8 is assumed when not specified;
313
- if (mediaType === 'application/graphql-response+json' &&
314
- charset === 'charset=utf8') {
315
- acceptedMediaType = 'application/graphql-response+json';
316
- break;
317
- }
318
- if ((mediaType === 'application/json' ||
319
- mediaType === 'application/*' ||
320
- mediaType === '*/*') &&
321
- charset === 'charset=utf8') {
322
- acceptedMediaType = 'application/json';
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
+ }
323
273
  break;
324
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');
325
320
  }
326
- return acceptedMediaType;
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;
327
328
  }
328
- exports.getAcceptableMediaType = getAcceptableMediaType;
329
329
  /**
330
330
  * Creates an appropriate GraphQL over HTTP response following the provided arguments.
331
331
  *
@@ -336,15 +336,13 @@ exports.getAcceptableMediaType = getAcceptableMediaType;
336
336
  *
337
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
338
  * error will be present in the `ExecutionResult` style.
339
- *
340
- * @category Server
341
339
  */
342
340
  function makeResponse(resultOrErrors, acceptedMediaType, formatError) {
343
341
  if (resultOrErrors instanceof Error &&
344
342
  // because GraphQLError extends the Error class
345
- !(0, utils_1.isGraphQLError)(resultOrErrors)) {
343
+ !isGraphQLError(resultOrErrors)) {
346
344
  return [
347
- JSON.stringify({ errors: [formatError(resultOrErrors)] }, utils_1.jsonErrorReplacer),
345
+ JSON.stringify({ errors: [formatError(resultOrErrors)] }, jsonErrorReplacer),
348
346
  {
349
347
  status: 400,
350
348
  statusText: 'Bad Request',
@@ -354,14 +352,14 @@ function makeResponse(resultOrErrors, acceptedMediaType, formatError) {
354
352
  },
355
353
  ];
356
354
  }
357
- const errors = (0, utils_1.isGraphQLError)(resultOrErrors)
355
+ const errors = isGraphQLError(resultOrErrors)
358
356
  ? [resultOrErrors]
359
- : (0, utils_1.areGraphQLErrors)(resultOrErrors)
357
+ : areGraphQLErrors(resultOrErrors)
360
358
  ? resultOrErrors
361
359
  : null;
362
360
  if (errors) {
363
361
  return [
364
- JSON.stringify({ errors: errors.map(formatError) }, utils_1.jsonErrorReplacer),
362
+ JSON.stringify({ errors: errors.map(formatError) }, jsonErrorReplacer),
365
363
  Object.assign(Object.assign({}, (acceptedMediaType === 'application/json'
366
364
  ? {
367
365
  status: 200,
@@ -379,7 +377,7 @@ function makeResponse(resultOrErrors, acceptedMediaType, formatError) {
379
377
  }
380
378
  return [
381
379
  JSON.stringify('errors' in resultOrErrors && resultOrErrors.errors
382
- ? Object.assign(Object.assign({}, resultOrErrors), { errors: resultOrErrors.errors.map(formatError) }) : resultOrErrors, utils_1.jsonErrorReplacer),
380
+ ? Object.assign(Object.assign({}, resultOrErrors), { errors: resultOrErrors.errors.map(formatError) }) : resultOrErrors, jsonErrorReplacer),
383
381
  {
384
382
  status: 200,
385
383
  statusText: 'OK',
@@ -391,10 +389,32 @@ function makeResponse(resultOrErrors, acceptedMediaType, formatError) {
391
389
  },
392
390
  ];
393
391
  }
394
- exports.makeResponse = makeResponse;
395
392
  function getHeader(req, key) {
396
393
  if (typeof req.headers.get === 'function') {
397
394
  return req.headers.get(key);
398
395
  }
399
396
  return Object(req.headers)[key];
400
397
  }
398
+ function areGraphQLErrors(obj) {
399
+ return (Array.isArray(obj) &&
400
+ obj.length > 0 &&
401
+ // if one item in the array is a GraphQLError, we're good
402
+ obj.some(isGraphQLError));
403
+ }
404
+ function isGraphQLError(obj) {
405
+ return obj instanceof graphql_1.GraphQLError;
406
+ }
407
+ function jsonErrorReplacer(_key,
408
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
409
+ val) {
410
+ if (val instanceof Error &&
411
+ // GraphQL errors implement their own stringer
412
+ !isGraphQLError(val)) {
413
+ return {
414
+ // name: val.name, name is included in message
415
+ message: val.message,
416
+ // stack: val.stack, can leak sensitive details
417
+ };
418
+ }
419
+ return val;
420
+ }