body-parser.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import HttpErrors from 'http-errors';
  2. import {Errorf} from '@e22m4u/js-format';
  3. import {createError} from '../utils/index.js';
  4. import {RouterOptions} from '../router-options.js';
  5. import {parseContentType} from '../utils/index.js';
  6. import {fetchRequestBody} from '../utils/index.js';
  7. import {DebuggableService} from '../debuggable-service.js';
  8. /**
  9. * Method names to be parsed.
  10. *
  11. * @type {string[]}
  12. */
  13. export const METHODS_WITH_BODY = ['POST', 'PUT', 'PATCH', 'DELETE'];
  14. /**
  15. * Unparsable media types.
  16. *
  17. * @type {string[]}
  18. */
  19. export const UNPARSABLE_MEDIA_TYPES = ['multipart/form-data'];
  20. /**
  21. * Body parser.
  22. */
  23. export class BodyParser extends DebuggableService {
  24. /**
  25. * Parsers.
  26. *
  27. * @type {{[mime: string]: Function}}
  28. */
  29. _parsers = {
  30. 'text/plain': v => String(v),
  31. 'application/json': parseJsonBody,
  32. };
  33. /**
  34. * Set parser.
  35. *
  36. * @param {string} mediaType
  37. * @param {Function} parser
  38. * @returns {this}
  39. */
  40. defineParser(mediaType, parser) {
  41. if (!mediaType || typeof mediaType !== 'string')
  42. throw new Errorf(
  43. 'The parameter "mediaType" of BodyParser.defineParser ' +
  44. 'should be a non-empty String, but %v given.',
  45. mediaType,
  46. );
  47. if (!parser || typeof parser !== 'function')
  48. throw new Errorf(
  49. 'The parameter "parser" of BodyParser.defineParser ' +
  50. 'should be a Function, but %v given.',
  51. parser,
  52. );
  53. this._parsers[mediaType] = parser;
  54. return this;
  55. }
  56. /**
  57. * Has parser.
  58. *
  59. * @param {string} mediaType
  60. * @returns {boolean}
  61. */
  62. hasParser(mediaType) {
  63. if (!mediaType || typeof mediaType !== 'string')
  64. throw new Errorf(
  65. 'The parameter "mediaType" of BodyParser.hasParser ' +
  66. 'should be a non-empty String, but %v given.',
  67. mediaType,
  68. );
  69. return Boolean(this._parsers[mediaType]);
  70. }
  71. /**
  72. * Delete parser.
  73. *
  74. * @param {string} mediaType
  75. * @returns {this}
  76. */
  77. deleteParser(mediaType) {
  78. if (!mediaType || typeof mediaType !== 'string')
  79. throw new Errorf(
  80. 'The parameter "mediaType" of BodyParser.deleteParser ' +
  81. 'should be a non-empty String, but %v given.',
  82. mediaType,
  83. );
  84. const parser = this._parsers[mediaType];
  85. if (!parser) throw new Errorf('The parser of %v is not found.', mediaType);
  86. delete this._parsers[mediaType];
  87. return this;
  88. }
  89. /**
  90. * Parse.
  91. *
  92. * @param {import('http').IncomingMessage} req
  93. * @returns {Promise<*>|undefined}
  94. */
  95. parse(req) {
  96. if (!METHODS_WITH_BODY.includes(req.method.toUpperCase())) {
  97. this.debug(
  98. 'Body parsing was skipped for the %s request.',
  99. req.method.toUpperCase(),
  100. );
  101. return;
  102. }
  103. const contentType = (req.headers['content-type'] || '').replace(
  104. /^([^;]+);.*$/,
  105. '$1',
  106. );
  107. if (!contentType) {
  108. this.debug(
  109. 'Body parsing was skipped because the request has no content type.',
  110. );
  111. return;
  112. }
  113. const {mediaType} = parseContentType(contentType);
  114. if (!mediaType)
  115. throw createError(
  116. HttpErrors.BadRequest,
  117. 'Unable to parse the "content-type" header.',
  118. );
  119. const parser = this._parsers[mediaType];
  120. if (!parser) {
  121. if (UNPARSABLE_MEDIA_TYPES.includes(mediaType)) {
  122. this.debug('Body parsing was skipped for %v.', mediaType);
  123. return;
  124. }
  125. throw createError(
  126. HttpErrors.UnsupportedMediaType,
  127. 'Media type %v is not supported.',
  128. mediaType,
  129. );
  130. }
  131. const bodyBytesLimit = this.getService(RouterOptions).requestBodyBytesLimit;
  132. return fetchRequestBody(req, bodyBytesLimit).then(rawBody => {
  133. if (rawBody != null) return parser(rawBody);
  134. return rawBody;
  135. });
  136. }
  137. }
  138. /**
  139. * Parse json body.
  140. *
  141. * @param {string} input
  142. * @returns {*|undefined}
  143. */
  144. export function parseJsonBody(input) {
  145. if (typeof input !== 'string') return undefined;
  146. try {
  147. return JSON.parse(input);
  148. } catch (error) {
  149. if (process.env['DEBUG'] || process.env['NODE_ENV'] === 'development')
  150. console.warn(error);
  151. throw createError(HttpErrors.BadRequest, 'Unable to parse request body.');
  152. }
  153. }