Skip to content

fix(basket): stop buffering an unbounded /errors body to measure it - #718

Merged
izadoesdev merged 2 commits into
stagingfrom
izadoesdev/bound-errors-body-read
Sep 3, 2026
Merged

fix(basket): stop buffering an unbounded /errors body to measure it#718
izadoesdev merged 2 commits into
stagingfrom
izadoesdev/bound-errors-body-read

Conversation

@izadoesdev

@izadoesdev izadoesdev commented Sep 3, 2026

Copy link
Copy Markdown
Member

Closes the last live cubic finding on the release PR (#714), apps/basket/src/routes/basket.ts:174.

The bug

markOversizedErrorsBody guards /errors with a 128 KB cap. When content-length is present it trusts the declared value, which is fine — the server stops reading at the length it was given, so a caller cannot understate it to smuggle more.

When the header is absent it did this:

const body = await request.clone().text();
return Buffer.byteLength(body) > ERRORS_BODY_MAX_BYTES ? OVERSIZED_ERRORS_BODY : undefined;

So the one path the guard exists to cover was also the one that let a client stream an arbitrary amount into memory before the size was ever checked.

The fix

Read the cloned stream chunk by chunk and stop at the first chunk that crosses the cap, so at most one chunk beyond the limit is ever held.

let chunk = await reader.read();
while (!chunk.done) {
  seenBytes += chunk.value.byteLength;
  if (seenBytes > ERRORS_BODY_MAX_BYTES) {
    reader.cancel();
    return OVERSIZED_ERRORS_BODY;
  }
  chunk = await reader.read();
}

The cancel() is deliberately not awaited. clone() tees the body, and awaiting the cancel of one branch while the other is never read deadlocks the request. I hit this: the first version of this fix used await reader.cancel() in a finally, and it hung both the new test and the pre-existing oversized body without content-length is still rejected test at 5s. Worth knowing before anyone "tidies" that line.

Verification

New test streams a body whose producer is willing to emit 1 MB and asserts the producer is stopped within 2× the cap, which is what proves the early bail rather than just the 413:

expect(res.status).toBe(413);
expect(produced).toBeLessThanOrEqual(ERRORS_BODY_MAX_BYTES * 2);

bunx vitest run src/routes/integration.test.ts → 57/57, including the pre-existing oversized-body tests. Basket check-types and test both re-run with --force to bypass the turbo cache. Repo lint clean, 14/14 policy tests.

Not addressed

Chunk granularity means the effective ceiling is the cap plus one chunk rather than exactly 128 KB. Bounding it precisely would mean truncating mid-chunk for a request that is being rejected anyway.


Summary by cubic

Fixes an unbounded memory read in the /errors route when a request lacks a content-length header. Previously the whole body was buffered into a string before the 128 KB cap was checked; now the cloned stream is read chunk by chunk and stops as soon as the cap is crossed.

Bug Fixes

  • Holds at most one chunk beyond the cap instead of the entire request body.
  • Leaves reader.cancel() unawaited because awaiting it deadlocks the teed request body; a .catch() handles a failed cancel so it doesn't surface as an unhandled rejection.
  • Adds a test that streams a 1 MB body and asserts the producer stops within 2× the cap.
  • The effective ceiling is now the cap plus one chunk, not exactly 128 KB.

Written for commit f1b737c. Summary will update on new commits.

Review in cubic

The no-content-length branch read the whole request into a string before
checking its size, so the one path the guard exists to cover was also the
one that let a client stream as much as it liked into memory.

Read the cloned stream chunk by chunk instead and stop at the first chunk
that crosses the cap, so at most one chunk beyond the limit is ever held.
The cancel is deliberately not awaited: clone() tees the body, and
awaiting the cancel of one branch while the other is never read deadlocks
the request.

The declared-content-length branch stays as it was. A caller cannot use
it to smuggle a larger body, because the server stops reading at the
length it was given.
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
databuddy-status Ready Ready Preview Sep 3, 2026 9:47pm UTC
2 Skipped Deployments
Project Deployment Actions Updated
dashboard Skipped Skipped Sep 3, 2026 9:47pm UTC
documentation Skipped Skipped Sep 3, 2026 9:47pm UTC

@unkey-deploy

unkey-deploy Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Unkey Deploy

Name Status Preview Inspect Updated (UTC)
links (preview) Ready Visit Preview Inspect Sep 3, 2026 9:47pm

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Team

Run ID: d17d871e-0f31-401d-a1d4-b86e6e6309af

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces unbounded buffering of chunked /errors request bodies with incremental byte counting and early cancellation after the 128 KB limit.

  • Reads cloned request streams one chunk at a time when content-length is unavailable.
  • Returns the oversized-body sentinel immediately after crossing the cap.
  • Adds an integration test verifying a 413 response and bounded producer consumption.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking async-style violation in the new stream-reading loop.

The incremental reader addresses the unbounded buffering path and the added test exercises early rejection; the only accepted concern is compliance with repository rules governing loop awaits and Promise handling.

Files Needing Attention: apps/basket/src/routes/basket.ts

Important Files Changed

Filename Overview
apps/basket/src/routes/basket.ts Replaces full-body buffering with bounded streaming, but the loop and ignored cancellation promise violate the repository's asynchronous-code rules.
apps/basket/src/routes/integration.test.ts Adds focused coverage demonstrating that oversized chunked bodies are rejected before the producer emits its full payload.

Reviews (1): Last reviewed commit: "fix(basket): stop buffering an unbounded..." | Re-trigger Greptile

Comment thread apps/basket/src/routes/basket.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 2 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would auto-approve. Fixes unbounded memory read on /errors when content-length is absent by streaming and stopping at the cap; adds a test proving the early bail. Bounded, well-tested bug fix.

Re-trigger cubic

Greptile flagged the floating promise from reader.cancel(). Attach a
catch so a failed cancel cannot surface as an unhandled rejection, and
say in the code why it is not awaited, since awaiting it deadlocks.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would auto-approve. Fixes unbounded memory read on /errors when content-length is absent by streaming and stopping at the cap; adds a test proving the early bail. Bounded, well-tested bug fix.

Re-trigger cubic

@izadoesdev
izadoesdev merged commit 9b7da0b into staging Sep 3, 2026
19 checks passed
@izadoesdev
izadoesdev deleted the izadoesdev/bound-errors-body-read branch September 3, 2026 21:50
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