diff --git a/README.md b/README.md index 73acf89e..10cd794e 100644 --- a/README.md +++ b/README.md @@ -91,16 +91,27 @@ specifies the number of bytes; if it is a string, the value is passed to the [bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults to `'100kb'`. +##### parser + +The `parser` option is the function called against the request body to convert +it to a Javascript object. If a `reviver` is supplied, it is supplied as the +second argument to this function. + +``` +parser(body, reviver) -> req.body +``` + +Defaults to `JSON.parse`. + ##### reviver -The `reviver` option is passed directly to `JSON.parse` as the second -argument. You can find more information on this argument +You can find more information on this argument [in the MDN documentation about JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Example.3A_Using_the_reviver_parameter). ##### strict When set to `true`, will only accept arrays and objects; when `false` will -accept anything `JSON.parse` accepts. Defaults to `true`. +accept anything the `parser` accepts. Defaults to `true`. ##### type @@ -279,6 +290,16 @@ The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)` where `buf` is a `Buffer` of the raw request body and `encoding` is the encoding of the request. The parsing can be aborted by throwing an error. +##### parser + +The `parser` option, if supplied, is used to in place of the default parser to +convert the request body into a Javascript object. If this option is supplied, +both the `extended` and `parameterLimit` options are ignored. + +``` +parser(body) -> req.body +``` + ## Errors The middlewares provided by this module create errors using the diff --git a/index.js b/index.js index 93c3a1ff..5a6e022e 100644 --- a/index.js +++ b/index.js @@ -81,6 +81,17 @@ Object.defineProperty(exports, 'urlencoded', { get: createParserGetter('urlencoded') }) +/** + * Generic parser used to build parsers. + * @public + */ + +Object.defineProperty(exports, 'generic', { + configurable: true, + enumerable: true, + get: createParserGetter('generic') +}) + /** * Create a middleware to parse json and urlencoded bodies. * @@ -150,6 +161,9 @@ function loadParser (parserName) { case 'urlencoded': parser = require('./lib/types/urlencoded') break + case 'generic': + parser = require('./lib/generic-parser') + break } // store to prevent invoking require() diff --git a/lib/generic-parser.js b/lib/generic-parser.js new file mode 100644 index 00000000..fb57f43f --- /dev/null +++ b/lib/generic-parser.js @@ -0,0 +1,168 @@ +/*! + * body-parser + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var bytes = require('bytes') +var contentType = require('content-type') +var createError = require('http-errors') +var debug = require('debug')('body-parser:generic') +var read = require('./read') +var typeis = require('type-is') + +/** + * Module exports. + */ + +module.exports = generic + +/** + * Use this to create a middleware that parses request bodies + * + * @param {object} [options] + * @return {function} + * @public + */ + +function generic (parserOptions, parserOverrides) { + var opts = {} + + // Squash the options and the overrides down into one object + var squashKey + for (squashKey in (parserOptions || {})) { + if (Object.prototype.hasOwnProperty.call(parserOptions, squashKey)) { + opts[squashKey] = parserOptions[squashKey] + } + } + for (squashKey in (parserOverrides || {})) { + if (Object.prototype.hasOwnProperty.call(parserOverrides, squashKey)) { + opts[squashKey] = parserOverrides[squashKey] + } + } + + var limit = typeof opts.limit !== 'number' + ? bytes.parse(opts.limit || '100kb') + : opts.limit + var charset = opts.charset + var inflate = opts.inflate !== false + var verify = opts.verify || false + var parse = opts.parse || defaultParse + var defaultReqCharset = opts.defaultCharset || 'utf-8' + var type = opts.type + + if (verify !== false && typeof verify !== 'function') { + throw new TypeError('option verify must be function') + } + + // create the appropriate type checking function + var shouldParse = typeof type !== 'function' + ? typeChecker(type) + : type + + // create the appropriate charset validating function + var validCharset = typeof charset !== 'function' + ? charsetValidator(charset) + : charset + + return function genericParser (req, res, next) { + if (req._body) { + debug('body already parsed') + next() + return + } + + req.body = req.body || {} + + // skip requests without bodies + if (!typeis.hasBody(req)) { + debug('skip empty body') + next() + return + } + + debug('content-type %j', req.headers['content-type']) + + // determine if request should be parsed + if (!shouldParse(req)) { + debug('skip parsing') + next() + return + } + + // assert charset per RFC 7159 sec 8.1 + var reqCharset = null + if (charset !== undefined) { + reqCharset = getCharset(req) || defaultReqCharset + if (!validCharset(reqCharset)) { + debug('invalid charset') + next(createError(415, 'unsupported charset "' + reqCharset.toUpperCase() + '"', { + charset: reqCharset, + type: 'charset.unsupported' + })) + return + } + } + + // read + read(req, res, next, parse, debug, { + encoding: reqCharset, + inflate: inflate, + limit: limit, + verify: verify + }) + } +} + +function defaultParse (buf) { + return buf +} + +/** + * Get the charset of a request. + * + * @param {object} req + * @api private + */ + +function getCharset (req) { + try { + return (contentType.parse(req).parameters.charset || '').toLowerCase() + } catch (e) { + return undefined + } +} + +/** + * Get the simple type checker. + * + * @param {string} type + * @return {function} + */ + +function typeChecker (type) { + return function checkType (req) { + return Boolean(typeis(req, type)) + } +} + +/** + * Get the simple charset validator. + * + * @param {string} type + * @return {function} + */ + +function charsetValidator (charset) { + return function validateCharset (reqCharset) { + return charset === reqCharset + } +} diff --git a/lib/types/json.js b/lib/types/json.js index 2971dc14..d9f10c52 100644 --- a/lib/types/json.js +++ b/lib/types/json.js @@ -12,12 +12,8 @@ * @private */ -var bytes = require('bytes') -var contentType = require('content-type') -var createError = require('http-errors') +var genericParser = require('../..').generic var debug = require('debug')('body-parser:json') -var read = require('../read') -var typeis = require('type-is') /** * Module exports. @@ -50,95 +46,45 @@ var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*(.)/ // eslint-disable-line no-cont function json (options) { var opts = options || {} - var limit = typeof opts.limit !== 'number' - ? bytes.parse(opts.limit || '100kb') - : opts.limit - var inflate = opts.inflate !== false var reviver = opts.reviver var strict = opts.strict !== false + var parser = opts.parser || JSON.parse var type = opts.type || 'application/json' - var verify = opts.verify || false - if (verify !== false && typeof verify !== 'function') { - throw new TypeError('option verify must be function') - } - - // create the appropriate type checking function - var shouldParse = typeof type !== 'function' - ? typeChecker(type) - : type + return genericParser(opts, { + type: type, - function parse (body) { - if (body.length === 0) { - // special-case empty json body, as it's a common client-side mistake - // TODO: maybe make this configurable or part of "strict" option - return {} - } + charset: function validateCharset (charset) { + return charset.substr(0, 4) === 'utf-' + }, - if (strict) { - var first = firstchar(body) - - if (first !== '{' && first !== '[') { - debug('strict violation') - throw createStrictSyntaxError(body, first) + parse: function parse (buf) { + if (buf.length === 0) { + // special-case empty json body, as it's a common client-side mistake + // TODO: maybe make this configurable or part of "strict" option + return {} } - } - - try { - debug('parse json') - return JSON.parse(body, reviver) - } catch (e) { - throw normalizeJsonSyntaxError(e, { - message: e.message, - stack: e.stack - }) - } - } - - return function jsonParser (req, res, next) { - if (req._body) { - debug('body already parsed') - next() - return - } - - req.body = req.body || {} - // skip requests without bodies - if (!typeis.hasBody(req)) { - debug('skip empty body') - next() - return - } + if (strict) { + var first = firstchar(buf) - debug('content-type %j', req.headers['content-type']) - - // determine if request should be parsed - if (!shouldParse(req)) { - debug('skip parsing') - next() - return - } + if (first !== '{' && first !== '[') { + debug('strict violation') + throw createStrictSyntaxError(parser, reviver, buf, first) + } + } - // assert charset per RFC 7159 sec 8.1 - var charset = getCharset(req) || 'utf-8' - if (charset.substr(0, 4) !== 'utf-') { - debug('invalid charset') - next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { - charset: charset, - type: 'charset.unsupported' - })) - return + try { + debug('parse json') + return parser(buf, reviver) + } catch (e) { + throw normalizeJsonSyntaxError(e, { + message: e.message, + stack: e.stack + }) + } } - - // read - read(req, res, next, parse, debug, { - encoding: charset, - inflate: inflate, - limit: limit, - verify: verify - }) - } + }) } /** @@ -150,12 +96,12 @@ function json (options) { * @private */ -function createStrictSyntaxError (str, char) { +function createStrictSyntaxError (parser, reviver, str, char) { var index = str.indexOf(char) var partial = str.substring(0, index) + '#' try { - JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation') + parser(partial, reviver); /* istanbul ignore next */ throw new SyntaxError('strict violation') } catch (e) { return normalizeJsonSyntaxError(e, { message: e.message.replace('#', char), @@ -176,21 +122,6 @@ function firstchar (str) { return FIRST_CHAR_REGEXP.exec(str)[1] } -/** - * Get the charset of a request. - * - * @param {object} req - * @api private - */ - -function getCharset (req) { - try { - return (contentType.parse(req).parameters.charset || '').toLowerCase() - } catch (e) { - return undefined - } -} - /** * Normalize a SyntaxError for JSON.parse. * @@ -215,16 +146,3 @@ function normalizeJsonSyntaxError (error, obj) { return error } - -/** - * Get the simple type checker. - * - * @param {string} type - * @return {function} - */ - -function typeChecker (type) { - return function checkType (req) { - return Boolean(typeis(req, type)) - } -} diff --git a/lib/types/raw.js b/lib/types/raw.js index f5d1b674..b40cfd5d 100644 --- a/lib/types/raw.js +++ b/lib/types/raw.js @@ -10,10 +10,7 @@ * Module dependencies. */ -var bytes = require('bytes') -var debug = require('debug')('body-parser:raw') -var read = require('../read') -var typeis = require('type-is') +var genericParser = require('../..').generic /** * Module exports. @@ -32,70 +29,9 @@ module.exports = raw function raw (options) { var opts = options || {} - var inflate = opts.inflate !== false - var limit = typeof opts.limit !== 'number' - ? bytes.parse(opts.limit || '100kb') - : opts.limit var type = opts.type || 'application/octet-stream' - var verify = opts.verify || false - if (verify !== false && typeof verify !== 'function') { - throw new TypeError('option verify must be function') - } - - // create the appropriate type checking function - var shouldParse = typeof type !== 'function' - ? typeChecker(type) - : type - - function parse (buf) { - return buf - } - - return function rawParser (req, res, next) { - if (req._body) { - debug('body already parsed') - next() - return - } - - req.body = req.body || {} - - // skip requests without bodies - if (!typeis.hasBody(req)) { - debug('skip empty body') - next() - return - } - - debug('content-type %j', req.headers['content-type']) - - // determine if request should be parsed - if (!shouldParse(req)) { - debug('skip parsing') - next() - return - } - - // read - read(req, res, next, parse, debug, { - encoding: null, - inflate: inflate, - limit: limit, - verify: verify - }) - } -} - -/** - * Get the simple type checker. - * - * @param {string} type - * @return {function} - */ - -function typeChecker (type) { - return function checkType (req) { - return Boolean(typeis(req, type)) - } + return genericParser(opts, { + type: type + }) } diff --git a/lib/types/text.js b/lib/types/text.js index 083a0090..87e65793 100644 --- a/lib/types/text.js +++ b/lib/types/text.js @@ -10,11 +10,7 @@ * Module dependencies. */ -var bytes = require('bytes') -var contentType = require('content-type') -var debug = require('debug')('body-parser:text') -var read = require('../read') -var typeis = require('type-is') +var genericParser = require('../..').generic /** * Module exports. @@ -34,88 +30,11 @@ function text (options) { var opts = options || {} var defaultCharset = opts.defaultCharset || 'utf-8' - var inflate = opts.inflate !== false - var limit = typeof opts.limit !== 'number' - ? bytes.parse(opts.limit || '100kb') - : opts.limit var type = opts.type || 'text/plain' - var verify = opts.verify || false - if (verify !== false && typeof verify !== 'function') { - throw new TypeError('option verify must be function') - } - - // create the appropriate type checking function - var shouldParse = typeof type !== 'function' - ? typeChecker(type) - : type - - function parse (buf) { - return buf - } - - return function textParser (req, res, next) { - if (req._body) { - debug('body already parsed') - next() - return - } - - req.body = req.body || {} - - // skip requests without bodies - if (!typeis.hasBody(req)) { - debug('skip empty body') - next() - return - } - - debug('content-type %j', req.headers['content-type']) - - // determine if request should be parsed - if (!shouldParse(req)) { - debug('skip parsing') - next() - return - } - - // get charset - var charset = getCharset(req) || defaultCharset - - // read - read(req, res, next, parse, debug, { - encoding: charset, - inflate: inflate, - limit: limit, - verify: verify - }) - } -} - -/** - * Get the charset of a request. - * - * @param {object} req - * @api private - */ - -function getCharset (req) { - try { - return (contentType.parse(req).parameters.charset || '').toLowerCase() - } catch (e) { - return undefined - } -} - -/** - * Get the simple type checker. - * - * @param {string} type - * @return {function} - */ - -function typeChecker (type) { - return function checkType (req) { - return Boolean(typeis(req, type)) - } + return genericParser(opts, { + type: type, + charset: function validateCharset () { return true }, + defaultCharset: defaultCharset + }) } diff --git a/lib/types/urlencoded.js b/lib/types/urlencoded.js index b2ca8f16..4920f421 100644 --- a/lib/types/urlencoded.js +++ b/lib/types/urlencoded.js @@ -12,13 +12,10 @@ * @private */ -var bytes = require('bytes') -var contentType = require('content-type') var createError = require('http-errors') var debug = require('debug')('body-parser:urlencoded') var deprecate = require('depd')('body-parser') -var read = require('../read') -var typeis = require('type-is') +var genericParser = require('../..').generic /** * Module exports. @@ -39,88 +36,34 @@ var parsers = Object.create(null) * @return {function} * @public */ - function urlencoded (options) { var opts = options || {} // notice because option default will flip in next major - if (opts.extended === undefined) { + if (opts.extended === undefined && opts.parser === undefined) { deprecate('undefined extended: provide extended option') } var extended = opts.extended !== false - var inflate = opts.inflate !== false - var limit = typeof opts.limit !== 'number' - ? bytes.parse(opts.limit || '100kb') - : opts.limit var type = opts.type || 'application/x-www-form-urlencoded' - var verify = opts.verify || false - - if (verify !== false && typeof verify !== 'function') { - throw new TypeError('option verify must be function') - } - - // create the appropriate query parser - var queryparse = extended - ? extendedparser(opts) - : simpleparser(opts) - - // create the appropriate type checking function - var shouldParse = typeof type !== 'function' - ? typeChecker(type) - : type - - function parse (body) { - return body.length - ? queryparse(body) - : {} - } - - return function urlencodedParser (req, res, next) { - if (req._body) { - debug('body already parsed') - next() - return - } - - req.body = req.body || {} - - // skip requests without bodies - if (!typeis.hasBody(req)) { - debug('skip empty body') - next() - return - } - - debug('content-type %j', req.headers['content-type']) - - // determine if request should be parsed - if (!shouldParse(req)) { - debug('skip parsing') - next() - return + var charset = opts.charset || 'utf-8' + + var queryparse = opts.parser || ( + extended + ? extendedparser(opts) + : simpleparser(opts) + ) + + return genericParser(opts, { + type: type, + charset: charset, + + parse: function parse (buf) { + return buf.length + ? queryparse(buf) + : {} } - - // assert charset - var charset = getCharset(req) || 'utf-8' - if (charset !== 'utf-8') { - debug('invalid charset') - next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { - charset: charset, - type: 'charset.unsupported' - })) - return - } - - // read - read(req, res, next, parse, debug, { - debug: debug, - encoding: charset, - inflate: inflate, - limit: limit, - verify: verify - }) - } + }) } /** @@ -165,21 +108,6 @@ function extendedparser (options) { } } -/** - * Get the charset of a request. - * - * @param {object} req - * @api private - */ - -function getCharset (req) { - try { - return (contentType.parse(req).parameters.charset || '').toLowerCase() - } catch (e) { - return undefined - } -} - /** * Count the number of parameters, stopping once limit reached * @@ -269,16 +197,3 @@ function simpleparser (options) { return parse(body, undefined, undefined, { maxKeys: parameterLimit }) } } - -/** - * Get the simple type checker. - * - * @param {string} type - * @return {function} - */ - -function typeChecker (type) { - return function checkType (req) { - return Boolean(typeis(req, type)) - } -} diff --git a/package.json b/package.json index 1ea75517..05f48b07 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ }, "scripts": { "lint": "eslint --plugin markdown --ext js,md .", - "test": "mocha --require test/support/env --reporter spec --check-leaks --bail test/", + "test": "mocha --require test/support/env --reporter spec --check-leaks --globals __core-js_shared__ --bail test/", "test-cov": "nyc --reporter=html --reporter=text npm test", "test-travis": "nyc --reporter=text npm test" } -} +} \ No newline at end of file diff --git a/test/json.js b/test/json.js index ae745712..74634f21 100644 --- a/test/json.js +++ b/test/json.js @@ -69,6 +69,18 @@ describe('bodyParser.json()', function () { .expect(200, '{"user":"tobi"}', done) }) + it('should use external parsers', function (done) { + request(createServer({ + parser: function (body) { + return { foo: 'bar' } + } + })) + .post('/') + .set('Content-Type', 'application/json') + .send('{"str":') + .expect(200, '{"foo":"bar"}', done) + }) + describe('when JSON is invalid', function () { before(function () { this.server = createServer() diff --git a/test/urlencoded.js b/test/urlencoded.js index e867c0f4..a5036a01 100644 --- a/test/urlencoded.js +++ b/test/urlencoded.js @@ -19,6 +19,16 @@ describe('bodyParser.urlencoded()', function () { .expect(200, '{"user":"tobi"}', done) }) + it('should parse x-www-form-urlencoded with custom parser', function (done) { + request(createServer({ + parser: function (input) { return input.toUpperCase() } + })) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send('user=tobi') + .expect(200, '"USER=TOBI"', done) + }) + it('should 400 when invalid content-length', function (done) { var urlencodedParser = bodyParser.urlencoded() var server = createServer(function (req, res, next) {