parseBody.js.flow 3.7 KB

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