Skip to content

fix: stop body parsers from responding twice to an oversized body - #356

Merged
dimdenGD merged 1 commit into
dimdenGD:mainfrom
nigrosimone:fix-body-limit-double-next
Jul 31, 2026
Merged

fix: stop body parsers from responding twice to an oversized body#356
dimdenGD merged 1 commit into
dimdenGD:mainfrom
nigrosimone:fix-body-limit-double-next

Conversation

@nigrosimone

Copy link
Copy Markdown
Contributor

Sending a chunked request body larger than a body parser limit crashes the server with an uncaught exception, so any client can take the process down.

What happens

onData calls next(new Error("Request entity too large")) as soon as the accumulated size passes the limit, but it keeps being called for every chunk that is still on the way, and the stream path emits end afterwards too. Each of those calls runs the error handler again. The first one answers the request, the second one throws Can't write body: Response was already sent, and nothing catches it.

A body sent with content-length is caught earlier by the header check, so it does not reach this path. A chunked body has no content-length, so the limit can only be caught while reading, which is where the repeat calls happen.

Reproduction

const express = require("ultimate-express");
const app = express();
app.use(express.json({ limit: "20b" }));
app.post("/", (req, res) => res.json({ ok: true }));
app.use((err, req, res, next) => res.status(413).json({ tooLarge: true }));

app.listen(3000, async () => {
    const body = new ReadableStream({
        start(c) {
            c.enqueue(new TextEncoder().encode('{"aaaaaaaaaa":'));
            c.enqueue(new TextEncoder().encode('"bbbbbbbbbbbbbbbbbbbb"}'));
            c.close();
        }
    });
    await fetch("http://127.0.0.1:3000/", {
        method: "POST", body, duplex: "half",
        headers: { "content-type": "application/json" }
    });
});

On main the server dies with Error: Can't write body: Response was already sent. Express 4 and Express 5 answer 413 and stay up.

Fix

Mark the parse as finished on the first outcome, whether that is the limit being hit or the body ending, and ignore whatever arrives after. The buffered chunks are dropped at the same time, so an oversized body is not held in memory while the error travels through the middleware chain.

Tests

Added tests/middlewares/body-limit-chunked.js, which sends an oversized chunked body and then a normal one to check the server is still answering. It fails on main and matches Express 4 and Express 5 output with this change.

Once the accumulated body passed the limit, onData called next() with an
error but kept being invoked for every remaining chunk, and the stream
path went on to emit 'end' as well. Each of those calls ran the error
handler again, so the second response threw "Response was already sent"
and took the process down with an uncaught exception.

A request with a chunked body over the limit was enough to trigger it,
which any client can send.

Mark the parse as finished on the first outcome and ignore anything that
arrives afterwards, dropping the buffered chunks at the same time so an
oversized body isn't kept in memory while the error propagates.
@dimdenGD
dimdenGD merged commit 739a96f into dimdenGD:main Jul 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants