Repository: expressjs/body-parser

Issue:
update release info and history.md
we need to update the release info and history.md for v2.0.0 to include all breaking changes that went into v2 pre-release versions

Discussion:
@Phillip9587: Fixed By #629 

Solution patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index c642246..dd8c9fc 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -42,7 +42,7 @@ jobs:
 
       # Initializes the CodeQL tools for scanning.
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
+        uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
         with:
           languages: ${{ matrix.language }}
           config: |
@@ -68,4 +68,4 @@ jobs:
       #   ./location_of_script_within_repo/buildscript.sh
 
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
\ No newline at end of file
+        uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
\ No newline at end of file
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 0d16dcb..5d50766 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -64,6 +64,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
+        uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
         with:
           sarif_file: results.sarif
diff --git a/HISTORY.md b/HISTORY.md
index d18686e..0e6fbd5 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,8 +1,10 @@
 unreleased
 =========================
 
+* refactor: move common request validation to read function
 * deps: 
   * type-is@^2.0.1
+  * iconv-lite@^0.7.0
 
 2.2.0 / 2025-03-27
 =========================
@@ -33,9 +35,26 @@ unreleased
 
 2.0.0 / 2024-09-10
 =========================
-* Propagate changes from 1.20.3 
-* add brotli support #406
-* Breaking Change: Node.js 18 is the minimum supported version
+
+## Breaking Changes
+* Node.js 18 is the minimum supported version
+* `req.body` is no longer always initialized to `{}`
+  - it is left `undefined` unless a body is parsed
+* Remove deprecated `bodyParser()` combination middleware
+* ~`urlencoded` parser now defaults `extended` to `false`~ as released, this is not the case, fixed in 2.0.1
+* `urlencoded` simple parser now uses `qs` module instead of `querystring` module
+
+## Features
+* Add brotli support #406
+* urlencoded: Add option to customize the depth with a default value of 32
+* urlencoded: Support iso-8859-1, utf8 sentinel, and numeric entities
+* Use `on-finished` to determine when body read
+
+## Dependencies
+* deps: raw-body@^3.0.0
+* deps: qs@6.12.3
+* deps: debug@3.1.0
+* deps: iconv-lite@0.5.2
 
 2.0.0-beta.2 / 2023-02-23
 =========================
diff --git a/README.md b/README.md
index 9fcd4c6..7fe2e56 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,11 @@ object after the middleware (i.e. `req.body`).
 The `json` function takes an optional `options` object that may contain any of
 the following keys:
 
+##### defaultCharset
+
+Specify the default character set for the json content if the charset is not
+specified in the `Content-Type` header of the request. Defaults to `utf-8`.
+
 ##### inflate
 
 When set to `true`, then deflated (compressed) bodies will be inflated; when
@@ -291,7 +296,7 @@ Whether to decode numeric entities such as `&#9786;` when parsing an iso-8859-1
 form. Defaults to `false`.
 
 
-#### depth
+##### depth
 
 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.
 
diff --git a/lib/read.js b/lib/read.js
index eee8b11..b3f2345 100644
--- a/lib/read.js
+++ b/lib/read.js
@@ -16,6 +16,8 @@ var getBody = require('raw-body')
 var iconv = require('iconv-lite')
 var onFinished = require('on-finished')
 var zlib = require('node:zlib')
+var hasBody = require('type-is').hasBody
+var { getCharset } = require('./utils')
 
 /**
  * Module exports.
@@ -36,14 +38,52 @@ module.exports = read
  */
 
 function read (req, res, next, parse, debug, options) {
+  if (onFinished.isFinished(req)) {
+    debug('body already parsed')
+    next()
+    return
+  }
+
+  if (!('body' in req)) {
+    req.body = undefined
+  }
+
+  // skip requests without bodies
+  if (!hasBody(req)) {
+    debug('skip empty body')
+    next()
+    return
+  }
+
+  debug('content-type %j', req.headers['content-type'])
+
+  // determine if request should be parsed
+  if (!options.shouldParse(req)) {
+    debug('skip parsing')
+    next()
+    return
+  }
+
+  var encoding = null
+  if (options?.skipCharset !== true) {
+    encoding = getCharset(req) || options.defaultCharset
+
+    // validate charset
+    if (!!options?.isValidCharset && !options.isValidCharset(encoding)) {
+      debug('invalid charset')
+      next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', {
+        charset: encoding,
+        type: 'charset.unsupported'
+      }))
+      return
+    }
+  }
+
   var length
   var opts = options
   var stream
 
   // read options
-  var encoding = opts.encoding !== null
-    ? opts.encoding
-    : null
   var verify = opts.verify
 
   try {
diff --git a/lib/types/json.js b/lib/types/json.js
index 078ce71..2d136e5 100644
--- a/lib/types/json.js
+++ b/lib/types/json.js
@@ -12,12 +12,9 @@
  * @private
  */
 
-var createError = require('http-errors')
 var debug = require('debug')('body-parser:json')
-var isFinished = require('on-finished').isFinished
 var read = require('../read')
-var typeis = require('type-is')
-var { getCharset, normalizeOptions } = require('../utils')
+var { normalizeOptions } = require('../utils')
 
 /**
  * Module exports.
@@ -51,7 +48,7 @@ var JSON_SYNTAX_REGEXP = /#+/g
  */
 
 function json (options) {
-  var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/json')
+  var normalizedOptions = normalizeOptions(options, 'application/json')
 
   var reviver = options?.reviver
   var strict = options?.strict !== false
@@ -84,49 +81,11 @@ function json (options) {
   }
 
   return function jsonParser (req, res, next) {
-    if (isFinished(req)) {
-      debug('body already parsed')
-      next()
-      return
-    }
-
-    if (!('body' in req)) {
-      req.body = undefined
-    }
-
-    // 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 charset = getCharset(req) || 'utf-8'
-    if (charset.slice(0, 4) !== 'utf-') {
-      debug('invalid charset')
-      next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
-        charset: charset,
-        type: 'charset.unsupported'
-      }))
-      return
-    }
-
-    // read
     read(req, res, next, parse, debug, {
-      encoding: charset,
-      inflate,
-      limit,
-      verify
+      ...normalizedOptions,
+
+      // assert charset per RFC 7159 sec 8.1
+      isValidCharset: (charset) => charset.slice(0, 4) === 'utf-'
     })
   }
 }
diff --git a/lib/types/raw.js b/lib/types/raw.js
index 3788ff2..95ba581 100644
--- a/lib/types/raw.js
+++ b/lib/types/raw.js
@@ -11,9 +11,7 @@
  */
 
 var debug = require('debug')('body-parser:raw')
-var isFinished = require('on-finished').isFinished
 var read = require('../read')
-var typeis = require('type-is')
 var { normalizeOptions } = require('../utils')
 
 /**
@@ -31,45 +29,18 @@ module.exports = raw
  */
 
 function raw (options) {
-  var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/octet-stream')
+  var normalizedOptions = normalizeOptions(options, 'application/octet-stream')
 
   function parse (buf) {
     return buf
   }
 
   return function rawParser (req, res, next) {
-    if (isFinished(req)) {
-      debug('body already parsed')
-      next()
-      return
-    }
-
-    if (!('body' in req)) {
-      req.body = undefined
-    }
-
-    // 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,
-      limit,
-      verify
+      ...normalizedOptions,
+
+      // Skip charset validation and parse the body as is
+      skipCharset: true
     })
   }
 }
diff --git a/lib/types/text.js b/lib/types/text.js
index 3e0ab1b..aa096a9 100644
--- a/lib/types/text.js
+++ b/lib/types/text.js
@@ -11,10 +11,8 @@
  */
 
 var debug = require('debug')('body-parser:text')
-var isFinished = require('on-finished').isFinished
 var read = require('../read')
-var typeis = require('type-is')
-var { getCharset, normalizeOptions } = require('../utils')
+var { normalizeOptions } = require('../utils')
 
 /**
  * Module exports.
@@ -31,50 +29,13 @@ module.exports = text
  */
 
 function text (options) {
-  var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'text/plain')
-
-  var defaultCharset = options?.defaultCharset || 'utf-8'
+  var normalizedOptions = normalizeOptions(options, 'text/plain')
 
   function parse (buf) {
     return buf
   }
 
   return function textParser (req, res, next) {
-    if (isFinished(req)) {
-      debug('body already parsed')
-      next()
-      return
-    }
-
-    if (!('body' in req)) {
-      req.body = undefined
-    }
-
-    // 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,
-      limit,
-      verify
-    })
+    read(req, res, next, parse, debug, normalizedOptions)
   }
 }
diff --git a/lib/types/urlencoded.js b/lib/types/urlencoded.js
index f993425..4d8750a 100644
--- a/lib/types/urlencoded.js
+++ b/lib/types/urlencoded.js
@@ -14,11 +14,9 @@
 
 var createError = require('http-errors')
 var debug = require('debug')('body-parser:urlencoded')
-var isFinished = require('on-finished').isFinished
 var read = require('../read')
-var typeis = require('type-is')
 var qs = require('qs')
-var { getCharset, normalizeOptions } = require('../utils')
+var { normalizeOptions } = require('../utils')
 
 /**
  * Module exports.
@@ -35,10 +33,9 @@ module.exports = urlencoded
  */
 
 function urlencoded (options) {
-  var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/x-www-form-urlencoded')
+  var normalizedOptions = normalizeOptions(options, 'application/x-www-form-urlencoded')
 
-  var defaultCharset = options?.defaultCharset || 'utf-8'
-  if (defaultCharset !== 'utf-8' && defaultCharset !== 'iso-8859-1') {
+  if (normalizedOptions.defaultCharset !== 'utf-8' && normalizedOptions.defaultCharset !== 'iso-8859-1') {
     throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1')
   }
 
@@ -52,49 +49,11 @@ function urlencoded (options) {
   }
 
   return function urlencodedParser (req, res, next) {
-    if (isFinished(req)) {
-      debug('body already parsed')
-      next()
-      return
-    }
-
-    if (!('body' in req)) {
-      req.body = undefined
-    }
-
-    // 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
-    var charset = getCharset(req) || defaultCharset
-    if (charset !== 'utf-8' && charset !== 'iso-8859-1') {
-      debug('invalid charset')
-      next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
-        charset: charset,
-        type: 'charset.unsupported'
-      }))
-      return
-    }
-
-    // read
     read(req, res, next, parse, debug, {
-      encoding: charset,
-      inflate,
-      limit,
-      verify
+      ...normalizedOptions,
+
+      // assert charset
+      isValidCharset: (charset) => charset === 'utf-8' || charset === 'iso-8859-1'
     })
   }
 }
diff --git a/lib/utils.js b/lib/utils.js
index eee5d95..c457aa6 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -64,6 +64,7 @@ function normalizeOptions (options, defaultType) {
     : options?.limit
   var type = options?.type || defaultType
   var verify = options?.verify || false
+  var defaultCharset = options?.defaultCharset || 'utf-8'
 
   if (verify !== false && typeof verify !== 'function') {
     throw new TypeError('option verify must be function')
@@ -78,6 +79,7 @@ function normalizeOptions (options, defaultType) {
     inflate,
     limit,
     verify,
+    defaultCharset,
     shouldParse
   }
 }
diff --git a/package.json b/package.json
index b5ad51a..6a3c46e 100644
--- a/package.json
+++ b/package.json
@@ -8,12 +8,16 @@
   ],
   "license": "MIT",
   "repository": "expressjs/body-parser",
+  "funding": {
+    "type": "opencollective",
+    "url": "https://opencollective.com/express"
+  },
   "dependencies": {
     "bytes": "^3.1.2",
     "content-type": "^1.0.5",
     "debug": "^4.4.0",
     "http-errors": "^2.0.0",
-    "iconv-lite": "^0.6.3",
+    "iconv-lite": "^0.7.0",
     "on-finished": "^2.4.1",
     "qs": "^6.14.0",
     "raw-body": "^3.0.0",



Test patch:
diff --git a/test/utils.js b/test/utils.js
index ee6df5d..364d383 100644
--- a/test/utils.js
+++ b/test/utils.js
@@ -10,6 +10,7 @@ describe('normalizeOptions(options, defaultType)', () => {
       assert.strictEqual(result.inflate, true)
       assert.strictEqual(result.limit, 100 * 1024) // 100kb in bytes
       assert.strictEqual(result.verify, false)
+      assert.strictEqual(result.defaultCharset, 'utf-8')
       assert.strictEqual(typeof result.shouldParse, 'function')
     }
   })
@@ -19,12 +20,14 @@ describe('normalizeOptions(options, defaultType)', () => {
       inflate: false,
       limit: '200kb',
       type: 'application/xml',
-      verify: () => {}
+      verify: () => {},
+      defaultCharset: 'iso-8859-1'
     }
     const result = normalizeOptions(options, 'application/json')
     assert.strictEqual(result.inflate, false)
     assert.strictEqual(result.limit, 200 * 1024) // 200kb in bytes
     assert.strictEqual(result.verify, options.verify)
+    assert.strictEqual(result.defaultCharset, 'iso-8859-1')
     assert.strictEqual(typeof result.shouldParse, 'function')
   })
 
@@ -41,6 +44,7 @@ describe('normalizeOptions(options, defaultType)', () => {
     assert.strictEqual(result.inflate, false)
     assert.strictEqual(result.limit, 200 * 1024) // 200kb in bytes
     assert.strictEqual(result.verify, options.verify)
+    assert.strictEqual(result.defaultCharset, 'utf-8')
     assert.strictEqual(typeof result.shouldParse, 'function')
     assert.strictEqual(result.additional, undefined)
     assert.strictEqual(result.something, undefined)
@@ -109,6 +113,18 @@ describe('normalizeOptions(options, defaultType)', () => {
         assert.strictEqual(result.shouldParse({ headers: { 'content-type': 'application/json' } }), true)
       })
     })
+
+    describe('defaultCharset', () => {
+      it('should return "utf-8" if defaultCharset is not provided', () => {
+        const result = normalizeOptions({}, 'application/json')
+        assert.strictEqual(result.defaultCharset, 'utf-8')
+      })
+
+      it('should accept a defaultCharset', () => {
+        const result = normalizeOptions({ defaultCharset: 'iso-8859-1' }, 'application/json')
+        assert.strictEqual(result.defaultCharset, 'iso-8859-1')
+      })
+    })
   })
 
   describe('defaultType', () => {