body-parser.js 4.0 KB

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