Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ This module provides the following parsers:
* [Raw body parser](#bodyparserrawoptions)
* [Text body parser](#bodyparsertextoptions)
* [URL-encoded form body parser](#bodyparserurlencodedoptions)
* [Generic body parser](#bodyparsergenericoptions)

Other body parsers you might be interested in:

Expand Down Expand Up @@ -300,6 +301,89 @@ form. Defaults to `false`.

The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible.

### bodyParser.generic([options])

Returns middleware that enables you to create custom body parsers while inheriting all the common functionality from body-parser. This is the recommended approach for building custom parsers for content types not covered by the built-in parsers.

The generic parser handles all the common concerns like:
- Content-type matching
- Charset detection and handling
- Body size limits and validation
- Decompression (gzip, brotli, deflate)
- Error standardization
- Buffer handling

You only need to provide the type to match and the parsing logic specific to your content type.

A new `body` object containing the parsed data is populated on the `request` object after the middleware (i.e. `req.body`). The structure of this object depends on what your custom parse function returns.

See [Custom parser examples](#custom-parser-examples) for examples of creating your own parsers based on the generic parser.

#### Options

The `generic` function takes an `options` object that may contain any of the following keys:

##### parse

**Required.** The `parse` function that converts the raw request body buffer into a JavaScript object. This function receives two arguments:

```js
function parse (buffer, charset) {
// Convert Buffer to parsed object
return parsedObject
}
```

- `buffer`: A Buffer containing the raw request body
- `charset`: The detected charset from Content-Type header or defaultCharset
- Return value becomes `req.body`
- **IMPORTANT**: This function MUST be synchronous and return the parsed result directly
- It cannot be an `async` function
- It cannot return a Promise

Your parse function will be called even for empty bodies (with a zero-length buffer), but not for requests with no body concept (like GET requests).

For empty bodies, consider following these conventions:
- For JSON-like parsers: Return `{}` (empty object)
- For text/raw-like parsers: Return the empty buffer/string as-is

##### type

**Required.** The `type` option is used to determine what media type the middleware will parse. This option can be:

- A string mime type (like `application/xml`)
- An extension name (like `xml`)
- A mime type with a wildcard (like `*/xml`)
- An array of any of the above
- A function that takes a request and returns a boolean

If not a function, the `type` option is passed directly to the [type-is](https://www.npmjs.com/package/type-is) library. If a function, it will be called as `fn(req)` and the request is parsed if it returns a truthy value.

##### defaultCharset

Specify the default character set for the content if the charset is not specified in the `Content-Type` header of the request. Defaults to `utf-8`.

##### charset

If specified, the charset of the request must match this option. If the request charset doesn't match, a 415 Unsupported Media Type error is returned.
This option can be:

- A string charset (like `utf-8`)
- An array of string charsets (like `['utf-8', 'utf-16']`)
- A function that takes the requests string charset and returns a boolean (like `(charset) => charset.startsWith('utf-')`)

##### inflate

When set to `true`, then deflated (compressed) bodies will be inflated; when `false`, deflated bodies are rejected. Defaults to `true`.

##### limit

Controls the maximum request body size. If this is a number, then the value 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'`.

##### verify

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.

## Errors

The middlewares provided by this module create errors using the
Expand Down Expand Up @@ -479,6 +563,147 @@ app.use(bodyParser.raw({ type: 'application/vnd.custom-type' }))
app.use(bodyParser.text({ type: 'text/html' }))
```

### Custom parser examples

These examples demonstrate how to create parsers for custom content types while leveraging the generic parser for common HTTP concerns.

#### XML Parser Example

Create a custom middleware for parsing XML requests:

```js
const express = require('express')
const bodyParser = require('body-parser')
const xmljs = require('xml-js')

const app = express()

// Create XML parser middleware
const xmlParser = bodyParser.generic({
// Accept both application/xml and text/xml
type: ['application/xml', 'text/xml', '+xml'],

// Set limits to prevent abuse
limit: '500kb',

// Validate that only 'utf-8' encoded responses are accepted
charset: 'utf-8',

parse: function (buf, charset) {
// Handle empty body case
if (buf.length === 0) return {}

try {
const result = xmljs.xml2js(buf.toString(charset), {
compact: true,
trim: true,
nativeType: true
})
return result
} catch (err) {
const error = new Error(`Invalid XML: ${err.message}`)
error.status = 400
throw error
}
}
})

// Use parser in routes
app.post('/api/xml', xmlParser, function (req, res) {
res.json(req.body)
})
```


#### Creating Your Own Parser Module

You can also create your own reusable parser module similar to the built-in parsers:

```js
const bodyParser = require('body-parser')

// Create a factory function for CSV parsing middleware
function csvParser (options) {
const opts = options || {}

const delimiter = opts.delimiter || ','
const hasHeaders = opts.hasHeaders !== false

return bodyParser.generic({
type: opts.type || ['text/csv', 'application/csv'],
limit: opts.limit,
inflate: opts.inflate,
verify: opts.verify,
charset: 'utf-8',

parse: function (buf, charset) {
// Handle empty body
if (buf.length === 0) return { rows: [] }

try {
const csvText = buf.toString(charset)
const lines = csvText.split(/\r?\n/).filter(line => line.trim())

if (lines.length === 0) return { rows: [] }

if (hasHeaders) {
const headers = lines[0].split(delimiter)
const rows = lines.slice(1).map(line => {
const values = line.split(delimiter)
const row = {}

headers.forEach((header, i) => {
row[header] = values[i]
})

return row
})

return { headers, rows }
} else {
const rows = lines.map(line => line.split(delimiter))
return { rows }
}
} catch (err) {
const error = new Error(`CSV parse error: ${err.message}`)
error.status = 400
throw error
}
}
})
}

module.exports = csvParser
```

Using the custom parser module:

```js
const express = require('express')
const csvParser = require('./csv-parser')

const app = express()

// Use with default options
app.post('/api/csv', csvParser(), function (req, res) {
res.json({
rowCount: req.body.rows.length,
data: req.body
})
})

// Or with custom options
app.post('/api/customcsv', csvParser({
limit: '250kb',
delimiter: ';',
hasHeaders: true
}), function (req, res) {
res.json(req.body)
})
```

This pattern makes it easy to create reusable, configurable custom parsers that follow the same interface as the built-in parsers.

## License

[MIT](LICENSE)
Expand Down
11 changes: 11 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* @property {Function} raw Raw parser
* @property {Function} text Text parser
* @property {Function} urlencoded URL-encoded parser
* @property {Function} generic Generic parser for custom body formats
*/

/**
Expand Down Expand Up @@ -60,6 +61,16 @@ Object.defineProperty(exports, 'urlencoded', {
get: () => require('./lib/types/urlencoded')
})

/**
* Generic parser for custom body formats.
* @public
*/
Object.defineProperty(exports, 'generic', {
configurable: true,
enumerable: true,
get: () => require('./lib/generic')
})

/**
* Create a middleware to parse json and urlencoded bodies.
*
Expand Down
104 changes: 104 additions & 0 deletions lib/generic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use strict'

/**
* Module dependencies.
* @private
*/

const createError = require('http-errors')
const debug = require('debug')('body-parser:generic')
const read = require('./read')
const { normalizeOptions } = require('./utils')

/**
* Module exports.
* @public
*/

module.exports = generic

/**
* Create a middleware to parse request bodies.
*
* @param {Object} [options]
* @param {Function} [options.parse] Function to parse body (required). This function:
* - Receives (buffer, charset) as arguments
* - Must be synchronous (cannot be async or return a Promise)
* - Will be called for requests with empty bodies (zero-length buffer)
* - Will NOT be called for requests with no body at all (e.g., typical GET requests)
* - Return value becomes req.body
* @param {String|String[]|Function} [options.type] Request content-type to match (required)
* @param {String|Number} [options.limit] Maximum request body size
* @param {Boolean} [options.inflate] Enable handling compressed bodies
* @param {Function} [options.verify] Verify body content
* @param {String} [options.defaultCharset] Default charset when not specified
* @param {String|String[]|Function} [options.charset] Expected charset(s) or function which returns a boolean (will respond with 415 if not matched)
* @returns {Function} middleware
* @public
*/

function generic (options) {
const opts = options || {}

if (typeof opts.parse !== 'function') {
throw new TypeError('option parse must be a function')
}

// For generic parser, type is a required option
if (opts.type === undefined || (typeof opts.type !== 'string' && typeof opts.type !== 'function' && !Array.isArray(opts.type))) {
throw new TypeError('option type must be specified for generic parser')
}

// Use the common options normalization function
const normalizedOptions = normalizeOptions(opts, opts.type)

debug('creating parser with options %j', {
limit: normalizedOptions.limit,
inflate: normalizedOptions.inflate,
defaultCharset: normalizedOptions.defaultCharset
})

let isValidCharset
if (typeof opts.charset === 'string') {
const expectedCharset = opts.charset.toLowerCase()
isValidCharset = function isValidCharset (charset) {
return charset === expectedCharset
}
} else if (Array.isArray(opts.charset)) {
const expectedCharsets = opts.charset.map((v) => String(v).toLowerCase())
isValidCharset = function isValidCharset (charset) {
return expectedCharsets.includes(charset)
}
} else if (typeof opts.charset === 'function') {
isValidCharset = opts.charset
}

const readOptions = {
...normalizedOptions,
isValidCharset,
returnBuffer: true
}

function wrappedParse (body, charset) {
debug('parse %d byte body', body.length)

try {
// Call the parse function
const result = opts.parse(body, charset)
debug('parsed as %o', result)
return result
} catch (err) {
debug('parse error: %s', err.message)

throw createError(400, err.message, {
body: body.toString().substring(0, 100),
charset,
type: 'entity.parse.failed'
})
}
}

return function genericParser (req, res, next) {
read(req, res, next, wrappedParse, debug, readOptions)
}
}
4 changes: 2 additions & 2 deletions lib/read.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function read (req, res, next, parse, debug, options) {

// set raw-body options
opts.length = length
opts.encoding = verify
opts.encoding = verify || options.returnBuffer
? null
: encoding

Expand Down Expand Up @@ -156,7 +156,7 @@ function read (req, res, next, parse, debug, options) {
var str = body
try {
debug('parse body')
str = typeof body !== 'string' && encoding !== null
str = typeof body !== 'string' && encoding !== null && options.returnBuffer !== true
? iconv.decode(body, encoding)
: body
req.body = parse(str, encoding)
Expand Down
Loading