Skip to content

fix: close the socket after refusing an oversized declared body - #19

Merged
EauDoon merged 2 commits into
mainfrom
fix/http-early-reject-destroys-request
Sep 6, 2026
Merged

fix: close the socket after refusing an oversized declared body#19
EauDoon merged 2 commits into
mainfrom
fix/http-early-reject-destroys-request

Conversation

@EauDoon

@EauDoon EauDoon commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Problem

readJson rejects a declared Content-Length above MAX_BODY_BYTES before reading or destroying anything. The streaming path below it already destroys the request, with a comment explaining why:

Destroy the request so the body stops streaming. Without this, the client keeps sending bytes and the loop continues until EOF, which is a cheap bandwidth-exhaustion DoS.

The early exit had no equivalent guard. A loopback client can declare Content-Length: 500000000, send a small prefix, and then trickle one byte every few seconds. Each byte resets Node's inactivity requestTimeout, so the request never completes and the socket stays open indefinitely, at almost no bandwidth cost. MAX_CONCURRENT_REQUESTS and maxRequestsPerSocket do not bound this because the handler has already returned.

Verification

Added HTTP sidecar closes an over-cap request instead of waiting for the declared body, which opens a raw socket, sends a POST declaring 5 MB with a partial body and no FIN, and waits up to 2s for the server to close.

  • Before the fix: closed: false after 2004ms (connection held open). The test fails.
  • After the fix: closed in ~2ms.

Change

Marking the request on the over-cap path and destroying it once the error response finishes, rather than destroying before the response. This was necessary: destroying first, mirroring the streaming path exactly, broke the existing HTTP sidecar rejects unsafe requests before state mutation test, which asserts the client receives a 413. The response contract is preserved; only the post-response socket state changes.

Checks

  • npm run check: 139/139 (baseline 137, plus the new regression test and a companion test that a valid proposal on the same route still returns 201).
  • No behavior change for content-encoding, invalid Content-Length, or media-type rejections: those still respond normally and are covered by the existing suite.

Devin Review

The streaming reader destroys the request as soon as the body passes the cap,
because otherwise a client keeps sending bytes and holds the sidecar open. The
declared `Content-Length` rejection ran earlier and had no such guard: it threw
before reading or destroying anything, so the request was never completed and
the connection stayed open for the bytes the sidecar had already refused.

A client can declare `Content-Length: 500000000` and trickle one byte every few
seconds, resetting `requestTimeout` indefinitely and pinning a socket per
connection. Verified against the reference server: an over-cap request with a
partial body held the connection open past a 2s bound, while the same request
on the streaming path was reset immediately.

Reject after the response is delivered rather than before it, so the documented
413 contract is unchanged: the request is marked, the error is sent, and the
socket is destroyed once the response finishes.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 1 potential issue.

Devin Review

Comment thread src/http-server.js
Comment on lines 31 to 32
const contentEncoding = request.headers["content-encoding"];
if (contentEncoding && contentEncoding.toLowerCase() !== "identity") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Early rejections retain oversized uploads

An oversized upload remains open when routing or encoding validation rejects it before readJson. A trickling client can retain sockets after receiving an error.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment thread src/http-server.js
Comment on lines +435 to +437
response.once("finish", () => {
request.socket?.end();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Oversized uploads still retain sockets

A half-open client can keep trickling an oversized body after socket.end() sends the 413 and FIN. The server retains each connection because its readable half remains open, preserving the resource-exhaustion vector.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@EauDoon
EauDoon merged commit 7cf59e7 into main Sep 6, 2026
7 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.

1 participant