Skip to content

fix: destroy the request as soon as the body exceeds the size cap so a flooding client cannot hold the sidecar open - #18

Merged
EauDoon merged 2 commits into
mainfrom
fix/http-destroy-body-after-size-cap
Sep 4, 2026
Merged

fix: destroy the request as soon as the body exceeds the size cap so a flooding client cannot hold the sidecar open#18
EauDoon merged 2 commits into
mainfrom
fix/http-destroy-body-after-size-cap

Conversation

@EauDoon

@EauDoon EauDoon commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Bug review of EauDoon/consequence-rail found:

  • MEDIUM: src/http-server.js readJson kept iterating over the request stream after the cumulative byte count crossed MAX_BODY_BYTES, just clearing the local chunks buffer and continuing. The client kept sending bytes, the loop ran until the connection's natural EOF, and the sidecar only returned 413 after the entire oversized body had been transferred. A hostile client could pin the sidecar on a slow socket to extend a cheap bandwidth-exhaustion DoS.

Fix: when the size cap is exceeded, call request.destroy() to terminate the underlying socket, then break out of the for-await loop. The 413 path then runs once with the partial body, no more bytes are accepted. The local chunks buffer is also no longer cleared on overflow because the socket is closed; this is the same final-state guarantee with less work in the loop.

Verified: node --test test/rail.test.js test/recovery-preflight.test.js runs 90 tests, all pass.


Devin Review

Bug review of EauDoon/consequence-rail found:
- HIGH: propose, reserveRecourse, issuePermit, and acceptRecoveryQualification all mutate the in-memory action record (or the actions Map) BEFORE the eventStore.append fires. transition() is documented as failure-atomic (it appends then mutates record.state), but the calling code overwrites that property for its own pre-state writes. A failing event append leaves the record with reservation / permit / recovery_preflight fields set, but no STATE_TRANSITION or ACTION_PROPOSED event. The action is now permanently wedged because the rail cannot retroactively insert the missing event.
- HIGH: PR #15 fixed the same class of bug in authorize and the transition helper, but the four methods above were not updated.

Fix: in each method, build the new field values into a local variable first, perform the eventStore.append (or transition, which appends), and only commit the field writes to the record after the append returns successfully. propose appends the ACTION_PROPOSED event before calling this.actions.set. reserveRecourse and issuePermit call transition() before writing record.reservation / record.permit. acceptRecoveryQualification appends the RECOVERY_PREFLIGHT_ACCEPTED event before writing record.recovery_preflight.

Verified: node --test test/rail.test.js runs 83 tests, all pass.
…a flooding client cannot hold the sidecar open

Bug review of EauDoon/consequence-rail found:
- MEDIUM: src/http-server.js readJson kept iterating over the request stream after the cumulative byte count crossed MAX_BODY_BYTES, just clearing the local chunks buffer and continuing. The client kept sending bytes, the loop ran until the connection's natural EOF, and the sidecar only returned 413 after the entire oversized body had been transferred. A hostile client could pin the sidecar on a slow socket to extend a cheap bandwidth-exhaustion DoS.

Fix: when the size cap is exceeded, call request.destroy() to terminate the underlying socket, then break out of the for-await loop. The 413 path then runs once with the partial body, no more bytes are accepted. The local chunks buffer is also no longer cleared on overflow because the socket is closed; this is the same final-state guarantee with less work in the loop.

Verified: node --test test/rail.test.js test/recovery-preflight.test.js runs 90 tests, all pass.
@EauDoon
EauDoon merged commit 6ed47cc into main Sep 4, 2026
@EauDoon
EauDoon deleted the fix/http-destroy-body-after-size-cap branch September 4, 2026 10:51

@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 potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread src/http-server.js
Comment on lines +66 to +67
request.destroy();
break;

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 streams lose error responses

When a streamed body exceeds the limit, request.destroy() closes its socket before the 413 response is sent. Clients receive an empty disconnect.

Prompt for agents
Preserve the early termination of oversized request uploads without destroying the socket before the HTTP error can be written. In src/http-server.js, readJson currently calls request.destroy(), then throws REQUEST_TOO_LARGE into the server catch handler. Since IncomingMessage.destroy() destroys the socket shared with ServerResponse, send() cannot deliver the 413. Rework this path so chunked or length-mismatched oversized requests stop consuming input and still produce the documented JSON 413 response. Add a raw-socket test that exceeds the limit without a declared oversized Content-Length and verifies both early termination and the response.
Devin Review

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

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