koa2-better-body
Version:
Full-featured [koa][] body parser! Support parsing text, buffer, json, json patch, json api, csp-report, multipart, form and urlencoded bodies. Works for koa@1, koa@2 and will work for koa@3.
278 lines (250 loc) • 7.49 kB
JavaScript
/**
* Module dependencies
*/
var utils = require('lazy-cache')(require)
/**
* Temporarily re-assign `require` to trick browserify and
* webpack into reconizing lazy dependencies.
*
* This tiny bit of ugliness has the huge dual advantage of
* only loading modules that are actually called at some
* point in the lifecycle of the application, whilst also
* allowing browserify and webpack to find modules that
* are depended on but never actually called.
*/
var fn = require
require = utils // eslint-disable-line no-undef, no-native-reassign, no-global-assign
/**
* Lazily required module dependencies
*/
require('extend-shallow', 'extend')
require('formidable')
require('koa-body-parsers', 'bodyParsers')
require('querystring')
/**
* Restore `require`
*/
require = fn // eslint-disable-line no-undef, no-native-reassign, no-global-assign
/**
* > Patch querystring logic. By default uses node's
* built-in `querystring` module, otherwise that is
* provided from `opts.querystring`. Works well with
* the `qs` module. You can pass it like that
*
* **Example**
*
* ```js
* app.use(body({
* querystring: require('qs')
* }))
* ```
*
* @param {String} `str` querystring
* @param {Object} `opts` loaded options
* @return {Object} parsed querystring object
* @api private
*/
utils.parseQs = function parseQs (str, opts) {
return opts.querystring
? opts.querystring.parse(str, opts)
: utils.querystring.parse(str, opts.delimiter, '=', opts)
}
/**
* > Default options that will be loaded. Pass `options` to overwrite them.
*
* @param {Object} `options`
* @return {Object}
* @api private
*/
utils.defaultOptions = function defaultOptions (options) {
options = typeof options === 'object' ? options : {}
var types = utils.defaultTypes(options.extendTypes)
options = utils.extend({
fields: false,
files: false,
multipart: true,
textLimit: false,
formLimit: false,
jsonLimit: false,
jsonStrict: true,
detectJSON: false,
bufferLimit: false,
buffer: false,
strict: true,
// query string `parse` options
delimiter: '&',
decodeURIComponent: utils.querystring.unescape,
maxKeys: 1000
}, options)
options.delimiter = options.sep || options.delimiter
options.formLimit = options.formLimit || options.urlencodedLimit
options.extendTypes = types
options.onerror = options.onЕrror || options.onerror
options.onerror = typeof options.onerror === 'function'
? options.onerror
: false
options.delimiter = typeof options.delimiter === 'string'
? options.delimiter
: '&'
if (typeof options.handler !== 'function') {
options.handler = function * noopHandler () {}
}
if (typeof options.detectJSON !== 'function') {
options.detectJSON = function detectJSON () {
return false
}
}
return options
}
/**
* > Only extend/overwrite default accept types.
*
* @param {Object} `types`
* @return {Object}
* @api private
*/
utils.defaultTypes = function defaultTypes (types) {
types = typeof types === 'object' ? types : {}
return utils.extend({
multipart: [
'multipart/form-data'
],
text: [
'text/*'
],
form: [
'application/x-www-form-urlencoded'
],
json: [
'application/json',
'application/json-patch+json',
'application/vnd.api+json',
'application/csp-report'
],
buffer: [
'text/*'
]
}, types)
}
/**
* > Is "valid" request method, according to IETF Draft.
*
* @see https://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-19#section-6.1
* @param {String} `method` koa request method
* @return {Boolean}
* @api private
*/
utils.isValid = function isValid (method) {
return ['GET', 'HEAD', 'DELETE'].indexOf(method.toUpperCase()) === -1
}
/**
* > Add `koa-body-parsers` to the koa context. In addition
* also adds the formidable as multipart parser.
*
* @param {Object} `ctx` koa context
* @param {Object} `opts` default options
* @return {Object} `ctx` koa context
* @api private
*/
utils.setParsers = function setParsers (ctx, opts) {
ctx.app.querystring = opts.querystring ||
opts.qs || // alias
ctx.app && ctx.app.querystring ||
ctx.app && ctx.app.qs || // alias
ctx.querystring ||
ctx.qs // alias
utils.bodyParsers(ctx)
ctx.request.multipart = utils.multipart.bind(ctx)
return ctx
}
/**
* > Formidable wrapper as multipart parser to make
* thunk that later can be yielded. Also allows you to pass
* formidable.IncomingForm instance to `options.IncomingForm`.
*
* @param {Object} `options` passed or default plugin options
* @param {Object} `ctx` koa context
* @return {Function} thunk
* @api private
*/
utils.multipart = function multipart (options) {
var ctx = this
return new Promise(function(resolve,reject){
var buff = ''
var fields = {}
var files = []
var form = options.IncomingForm instanceof utils.formidable.IncomingForm
? options.IncomingForm
: new utils.formidable.IncomingForm(options)
form.on('error', reject)
form.on('aborted', reject)
form.on('file', function (name, value) {
files.push(value)
fields[name] = fields[name] || []
fields[name].push(value)
})
form.on('field', function (name, value) {
buff += name + '=' + value + options.delimiter
})
form.on('end', function () {
fields = buff && buff.length
? utils.extend({}, utils.parseQs(buff.slice(0, -1), options), fields)
: fields
resolve({
fields: fields,
files: files
})
})
form.parse(ctx.req)
})
}
/**
* > Parse a different type of request bodies. By default accepts
* and can parse JSON, JSON-API, JSON-Patch, text, form, urlencoded
* and buffer bodies.
*
* @param {Object} `ctx` koa context
* @param {Object} `options` plugin options
* @param {Function} `next` next middleware
* @api private
*/
utils.parseBody = async function parseBody (ctx, options, next) { /* eslint complexity: [2, 12] */
var fields = typeof options.fields === 'string' ? options.fields : 'fields'
var files = typeof options.files === 'string' ? options.files : 'files'
var custom = options.extendTypes.custom
if (custom && custom.length && ctx.request.is(custom)) {
await options.handler.call(ctx, ctx, options, next)
return next
}
if (options.detectJSON(ctx) || ctx.request.is(options.extendTypes.json)) {
ctx.app.jsonStrict = typeof options.jsonStrict === 'boolean' ? options.jsonStrict : true
ctx.request[fields] = await ctx.request.json(options.jsonLimit)
return next
}
if (ctx.request.is(options.extendTypes.form || options.extendTypes.urlencoded)) {
var res = await ctx.request.urlencoded(options.formLimit)
ctx.request[fields] = res
return next
}
if (options.buffer && ctx.request.is(options.extendTypes.buffer)) {
ctx.request.body = await ctx.request.buffer(options.bufferLimit)
return next
}
if (ctx.request.is(options.extendTypes.text)) {
var limit = options.textLimit
var body = await ctx.request.text(limit)
ctx.request.body = body
return next
}
if (options.multipart && ctx.request.is(options.extendTypes.multipart)) {
var result = await ctx.request.multipart(options)
ctx.request[fields] = result.fields
ctx.request[files] = result.files
return next
}
}
/**
* Expose `utils` modules
*/
module.exports = utils