From a38a3d9fe467de188dcc820d19e33a315933bdcc Mon Sep 17 00:00:00 2001 From: Nigro Simone Date: Sat, 1 Aug 2026 08:23:07 +0200 Subject: [PATCH] fix: give an empty request body the shape express gives it A request with content-length: 0 returned early before any parser ran, leaving req.body as the placeholder object every parser starts from. Express runs the parser regardless and each one produces its own empty value, so app.post('/upload', express.raw(), (req, res) => { hash.update(req.body); // works on express, throws on uExpress }) got a Buffer on express and a plain object here. Same for express.text(), where express hands back '' and this handed back an object. The empty case now runs the parser tail directly with an empty buffer, after the content-type checks so a parser still ignores a request it does not handle. Nothing is read, so this costs an allocation of a zero-length buffer. All four tails already handled an empty buffer - json even has an explicit branch for it - so each one produces what it should: {} for json and urlencoded, '' for text, an empty Buffer for raw. Verified against express 4 and express 5, which agree with each other here, and the new test now matches both. --- src/middlewares.js | 11 +++-- tests/tests/middlewares/empty-body-parsers.js | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 tests/tests/middlewares/empty-body-parsers.js diff --git a/src/middlewares.js b/src/middlewares.js index 255098e..eb8da02 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -181,10 +181,6 @@ function createBodyParser(defaultType, beforeReturn) { } const length = req.headers['content-length']; - // skip reading empty body - if(length == '0') { - return next(); - } if(options.simpleType) { const semicolonIndex = type.indexOf(';'); @@ -204,6 +200,13 @@ function createBodyParser(defaultType, beforeReturn) { } } + // an empty body still has to produce this parser's empty value the way express does - + // {} for json and urlencoded, '' for text, an empty Buffer for raw - rather than leaving + // req.body as the placeholder object. there is nothing to read, so run the tail directly + if(length == '0') { + return beforeReturn(req, res, next, options, Buffer.alloc(0)); + } + // skip reading too large body if(length && +length > options.limit) { return next(new Error('Request entity too large')); diff --git a/tests/tests/middlewares/empty-body-parsers.js b/tests/tests/middlewares/empty-body-parsers.js new file mode 100644 index 0000000..31fc9b5 --- /dev/null +++ b/tests/tests/middlewares/empty-body-parsers.js @@ -0,0 +1,49 @@ +// must give an empty body the same shape express does, for every body parser + +const express = require("express"); + +const app = express(); + +app.post('/json', express.json(), (req, res) => { + res.json({ type: typeof req.body, isBuffer: Buffer.isBuffer(req.body), value: JSON.stringify(req.body) }); +}); + +app.post('/urlencoded', express.urlencoded({ extended: false }), (req, res) => { + res.json({ type: typeof req.body, keys: Object.keys(req.body).length }); +}); + +app.post('/text', express.text(), (req, res) => { + res.json({ type: typeof req.body, length: req.body.length }); +}); + +app.post('/raw', express.raw(), (req, res) => { + res.json({ isBuffer: Buffer.isBuffer(req.body), length: req.body.length }); +}); + +function empty(path, contentType) { + return fetch(`http://localhost:13333${path}`, { + method: 'POST', + body: '', + headers: { 'Content-Type': contentType } + }).then(r => r.text()); +} + +app.listen(13333, async () => { + console.log('Server is running on port 13333'); + + console.log(await empty('/json', 'application/json')); + console.log(await empty('/urlencoded', 'application/x-www-form-urlencoded')); + console.log(await empty('/text', 'text/plain')); + console.log(await empty('/raw', 'application/octet-stream')); + + // a non-empty body must still parse the same way afterwards + const after = await fetch('http://localhost:13333/json', { + method: 'POST', + body: '{"a":1}', + headers: { 'Content-Type': 'application/json' } + }); + console.log(await after.text()); + + await new Promise(resolve => setTimeout(resolve, 100)); + process.exit(0); +});