diff --git a/benchmark/scenarios/body-json-512kb.js b/benchmark/scenarios/body-json-512kb.js new file mode 100644 index 00000000..1b8c6da6 --- /dev/null +++ b/benchmark/scenarios/body-json-512kb.js @@ -0,0 +1,28 @@ +'use strict'; + +// The other body-parser scenario posts 57 bytes, which uWS hands over in a single +// chunk, so it never exercises the accumulate path. This one is large enough to +// arrive in several chunks and makes the cost of collecting a body visible. +const PAD = 512 * 1024; + +module.exports = { + name: 'middlewares/body-json-512kb', + path: '/abc', + wrk: { + script: 'post-json-512kb.lua', + connections: 50 + }, + verify: { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ n: 1, pad: 'x'.repeat(PAD) }) + }, + setup(app, express) { + app.use(express.json({ limit: '10mb' })); + app.post('/abc', (req, res) => { + res.send(`${req.body.pad.length}`); + }); + } +}; diff --git a/benchmark/wrk-scripts/post-json-512kb.lua b/benchmark/wrk-scripts/post-json-512kb.lua new file mode 100644 index 00000000..41686fe4 --- /dev/null +++ b/benchmark/wrk-scripts/post-json-512kb.lua @@ -0,0 +1,7 @@ +wrk.method = "POST" +wrk.path = "/abc" +wrk.headers["Content-Type"] = "application/json" + +local kb = 1024 +local pad = string.rep("x", 512 * kb) +wrk.body = '{"n":1,"pad":"' .. pad .. '"}' diff --git a/src/middlewares.js b/src/middlewares.js index 36ed48e2..ffe7b81c 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -267,6 +267,32 @@ function createBodyParser(defaultType, beforeReturn) { // if we are fast enough (not async), we can do it // otherwise we need to use a stream since it already started streaming it if(!req.receivedData) { + // uWS can accumulate the whole body natively and hand it over in a single call, + // which skips one JS callback per chunk and the final Buffer.concat. + // not usable when inflating, since options.limit applies to the decompressed + // size while collectBody can only cap the compressed bytes it receives. + if(!inflate && Number.isFinite(options.limit)) { + req._res.collectBody(options.limit, ab => { + // uWS returns null when the body exceeded the limit + if(ab === null) { + return next(new Error('Request entity too large')); + } + // uWS may hand over memory it owns and detaches right after this call, + // so only parsers that don't keep the buffer around can view it directly + const buf = beforeReturn.retainsBuffer || options.verify + ? Buffer.from(ab.slice(0)) + : Buffer.from(ab); + if(options.verify) { + try { + options.verify(req, res, buf); + } catch(e) { + return next(e); + } + } + beforeReturn(req, res, next, options, buf); + }); + return; + } req._res.onData((ab, isLast) => { onData(ab); if(isLast) { @@ -300,10 +326,14 @@ const json = createBodyParser('application/json', function(req, res, next, optio next(); }); -const raw = createBodyParser('application/octet-stream', function(req, res, next, options, buf) { +function rawParser(req, res, next, options, buf) { req.body = buf; next(); -}); +} +// req.body outlives the parser, so this one needs its own copy of the body +rawParser.retainsBuffer = true; + +const raw = createBodyParser('application/octet-stream', rawParser); const text = createBodyParser('text/plain', function(req, res, next, options, buf) { let contentType = req.headers['content-type'];