Skip to content

Bound the bot's --feeds-url download - #138

Merged
thossullivan merged 3 commits into
mainfrom
fix/bot-feeds-url-bounds
Oct 25, 2026
Merged

thossullivan merged 3 commits into
mainfrom
fix/bot-feeds-url-bounds

Conversation

@thossullivan

Copy link
Copy Markdown
Owner

Fixes #134.

Summary

  • The 30 s / 8 MiB bounded reader moves unchanged from refresh/fetch.mjs to lib/fetch-bounded.mjs, which is in the npm package. refresh/fetch.mjs re-exports it.
  • downloadFeeds reads --feeds-url responses through fetchBoundedBody. An adapter keeps the legacy injected shapes working, and the cap applies to each of them.
  • A bound violation is an ordinary download failure. With feeds.allow_vendored_fallback the bot falls back with the same warning; without it, the bot stops.

What a bot user can notice

  • A download that takes over 30 s or exceeds 8 MiB now fails.
  • Error text changes: URL returned HTTP 500 becomes --feeds-url fetch failed for URL: HTTP 500 - <detail>.
  • Requests send Accept-Language: en and an abort signal.

Tests

  • 9 new bot assertions with an injected fetch: stalled request, stalled body, oversized stream, oversized Content-Length, oversized legacy shapes, every legacy shape still downloading, and fallback on a bound violation. All 9 fail on main.
  • npm test passes. Refresh test output is unchanged.

🤖 Generated with Claude Code

downloadFeeds called fetch(url) with no deadline and no size limit, so
a slow or huge response held the bot until the job timeout. The refresh
side already had a 30 second deadline and an 8 MiB cap in
refresh/fetch.mjs, but refresh/ is not in the npm package, so the bot
could not use it.

The bounded reader moves unchanged to lib/fetch-bounded.mjs, and
refresh/fetch.mjs re-exports it, so refresh behavior is byte-identical.
downloadFeeds now reads through fetchBoundedBody. An adapter keeps the
legacy injected response shapes (text(), string body, data, plain
object) working, and each of them is measured against the cap. A bound
violation is a download failure like any other, so vendored fallback
and its warning are unchanged.

Fixes #134.

Implemented by an Opus subagent; reviewed by Claude.
readBoundedBody and readErrorDetail added chunk.byteLength, which a
string chunk does not have, so the count became NaN. A body that
streamed strings then skipped the 8 MiB cap, and an error body that
streamed strings was read to the end. Both now count the bytes of
Buffer.from(chunk), the same buffer they already kept. For a Uint8Array
chunk that buffer has the chunk's byteLength, so refresh, which only
sees Uint8Array chunks, counts exactly as before.

A text() result that is a Buffer or Uint8Array is now decoded as UTF-8.
A Buffer already worked through String() and on main; a Uint8Array
stringified to a comma list and failed as invalid JSON.

The instanceof Response pass-through is removed. The duck-typed path
gives a real Response the same status, ok, headers, and body, so the
branch could not change any result.

New tests cover byte and string text() results, duck-typed and
string-streamed bodies above and below the cap, a string-streamed error
body read only to the detail cap, and legacy error shapes with and
without detail.

Implemented by an Opus subagent; reviewed by an Opus adversarial review.
A late-pass review found that dropping the ok !== false check from the
adapter passed the whole suite, so a 302 Response or a legacy ok: false
shape with a feed body would have been accepted. Both now have a test,
and both fail with the check removed.

Implemented by Claude; reviewed by an Opus late-pass review.
@thossullivan
thossullivan merged commit b6c53c8 into main Oct 25, 2026
6 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.

Bound the bot's --feeds-url download

1 participant