Skip to content

fix(fetcher): stream-cap fetchXml body instead of buffering the whole ZIP (#231)#245

Merged
williamzujkowski merged 1 commit into
mainfrom
fix/fetchxml-bounded-buffering-231
Jun 30, 2026
Merged

fix(fetcher): stream-cap fetchXml body instead of buffering the whole ZIP (#231)#245
williamzujkowski merged 1 commit into
mainfrom
fix/fetchxml-bounded-buffering-231

Conversation

@williamzujkowski

Copy link
Copy Markdown
Collaborator

Summary

fetchXml read the entire response with Buffer.from(await result.value.arrayBuffer()) and only checked buffer.length > MAX_DOWNLOAD_BYTES afterward. The Content-Length pre-check is the primary guard, but a chunked / untruthful-Content-Length response slips past it — so an oversized or unbounded body was fully materialized into memory before the length check, a runner-OOM vector.

Fix

  • Extract the streaming size-guard already used by readBodyCapped into readBytesCapped(response, maxBytes): Buffer | null — it streams the body and aborts (reader.cancel()) the instant the running total exceeds the cap, so an oversized body is never fully buffered.
  • readBodyCapped becomes a thin UTF-8 decoder over readBytesCapped.
  • fetchXml now reads via readBytesCapped and returns the same 'Download exceeds size limit' error on overflow — memory-bounded for the no-/false-Content-Length case the pre-check can't catch.

Tests

New readBytesCapped tests: returns full buffer under cap; returns null and cancels the stream once a no-Content-Length body exceeds the cap (proving it doesn't buffer everything); no-body arrayBuffer fallback. Existing fetchXml tests (valid ZIP, unchanged-hash, Content-Length reject, non-ZIP) still pass.

Full fetcher suite green (162 tests); lint + typecheck clean.

Closes #231

… ZIP (#231)

fetchXml read the entire response with `Buffer.from(await arrayBuffer())` and
only checked the size afterward. The Content-Length pre-check is the primary
guard, but a chunked or untruthful-Content-Length response slips past it, so an
oversized/unbounded body was fully materialized into memory before the
length check — a runner-OOM vector.

Extract the streaming size-guard from readBodyCapped into readBytesCapped
(Response -> Buffer | null), which aborts and cancels the stream the moment the
running total exceeds the cap. readBodyCapped becomes a thin UTF-8 decoder over
it. fetchXml now uses readBytesCapped, so the body is never fully buffered when
oversized.

Adds readBytesCapped tests (under-cap buffer, over-cap abort+cancel on a
no-Content-Length stream, no-body fallback).

Closes #231

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@williamzujkowski williamzujkowski requested a review from a team as a code owner June 30, 2026 01:40
@williamzujkowski williamzujkowski merged commit c450e50 into main Jun 30, 2026
3 checks passed
@williamzujkowski williamzujkowski deleted the fix/fetchxml-bounded-buffering-231 branch June 30, 2026 01:55
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.

security(fetcher): fetchXml buffers the entire ZIP via arrayBuffer() before the size check — largest download not memory-bounded

1 participant