parseBody.js.flow 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. // @flow strict
  2. import { type IncomingMessage } from 'http';
  3. import zlib from 'zlib';
  4. import getBody from 'raw-body';
  5. import httpError from 'http-errors';
  6. import querystring from 'querystring';
  7. import contentType from 'content-type';
  8. type $Request = IncomingMessage & { body?: ?mixed, ... };
  9. /**
  10. * Provided a "Request" provided by express or connect (typically a node style
  11. * HTTPClientRequest), Promise the body data contained.
  12. */
  13. export async function parseBody(
  14. req: $Request,
  15. ): Promise<{ [param: string]: mixed, ... }> {
  16. const { body } = req;
  17. // If express has already parsed a body as a keyed object, use it.
  18. if (typeof body === 'object' && !(body instanceof Buffer)) {
  19. return (body: any);
  20. }
  21. // Skip requests without content types.
  22. if (req.headers['content-type'] === undefined) {
  23. return {};
  24. }
  25. const typeInfo = contentType.parse(req);
  26. // If express has already parsed a body as a string, and the content-type
  27. // was application/graphql, parse the string body.
  28. if (typeof body === 'string' && typeInfo.type === 'application/graphql') {
  29. return { query: body };
  30. }
  31. // Already parsed body we didn't recognise? Parse nothing.
  32. if (body) {
  33. return {};
  34. }
  35. const rawBody = await readBody(req, typeInfo);
  36. // Use the correct body parser based on Content-Type header.
  37. switch (typeInfo.type) {
  38. case 'application/graphql':
  39. return { query: rawBody };
  40. case 'application/json':
  41. if (jsonObjRegex.test(rawBody)) {
  42. /* eslint-disable no-empty */
  43. try {
  44. return JSON.parse(rawBody);
  45. } catch (error) {
  46. // Do nothing
  47. }
  48. /* eslint-enable no-empty */
  49. }
  50. throw httpError(400, 'POST body sent invalid JSON.');
  51. case 'application/x-www-form-urlencoded':
  52. return querystring.parse(rawBody);
  53. }
  54. // If no Content-Type header matches, parse nothing.
  55. return {};
  56. }
  57. /**
  58. * RegExp to match an Object-opening brace "{" as the first non-space
  59. * in a string. Allowed whitespace is defined in RFC 7159:
  60. *
  61. * ' ' Space
  62. * '\t' Horizontal tab
  63. * '\n' Line feed or New line
  64. * '\r' Carriage return
  65. */
  66. const jsonObjRegex = /^[ \t\n\r]*\{/;
  67. // Read and parse a request body.
  68. async function readBody(req, typeInfo) {
  69. const charset = (typeInfo.parameters.charset || 'utf-8').toLowerCase();
  70. // Assert charset encoding per JSON RFC 7159 sec 8.1
  71. if (charset.slice(0, 4) !== 'utf-') {
  72. throw httpError(415, `Unsupported charset "${charset.toUpperCase()}".`);
  73. }
  74. // Get content-encoding (e.g. gzip)
  75. const contentEncoding = req.headers['content-encoding'];
  76. const encoding =
  77. typeof contentEncoding === 'string'
  78. ? contentEncoding.toLowerCase()
  79. : 'identity';
  80. const length = encoding === 'identity' ? req.headers['content-length'] : null;
  81. const limit = 100 * 1024; // 100kb
  82. const stream = decompressed(req, encoding);
  83. // Read body from stream.
  84. try {
  85. return await getBody(stream, { encoding: charset, length, limit });
  86. } catch (err) {
  87. throw err.type === 'encoding.unsupported'
  88. ? httpError(415, `Unsupported charset "${charset.toUpperCase()}".`)
  89. : httpError(400, `Invalid body: ${err.message}.`);
  90. }
  91. }
  92. // Return a decompressed stream, given an encoding.
  93. function decompressed(req, encoding) {
  94. switch (encoding) {
  95. case 'identity':
  96. return req;
  97. case 'deflate':
  98. return req.pipe(zlib.createInflate());
  99. case 'gzip':
  100. return req.pipe(zlib.createGunzip());
  101. }
  102. throw httpError(415, `Unsupported content-encoding "${encoding}".`);
  103. }