fix(dashboard): stream HTTP response bodies to EOF instead of one chunk (#4829) - #4858
Conversation
…nk (#4829) Three dashboard HTTP readers assumed a single StreamReader.read(cap + 1) returns the whole response body. read(n) returns UP TO n bytes, resolving as soon as any data is buffered, so on a chunked response with no Content-Length it returns only the first buffered chunk (~8-12 KiB) and the caller silently works on a truncated body: - updates._fetch_feed_bytes: release feed parsed from a partial document, so an update check can misread or fail on a chunked feed. The comment claiming an oversized body "is DETECTED rather than silently truncated" was false for a chunked feed and is corrected. - feedback._read_capped_text: a truncated Aperture response was decoded and treated as the full reply. - source_providers Jira fetch: json.loads on a partial document surfaced as SourceProviderError, so the issue read as unavailable. Same defect class as the official-registry site fixed in #4814. Since the single-read shape has been reintroduced independently more than once, extract one shared read_capped_response(resp, cap) helper in the dashboard handlers' _shared module (next to the request-side read_bounded_json) that drains iter_chunked chunks to EOF, enforcing the cap against the accumulated total so an oversized body is refused mid-stream, and clamping the return to cap + 1 bytes so every caller keeps its existing over-cap sentinel unchanged. All three sites route through it; cap values and caller semantics are untouched. Tests lock in: a multi-chunk body is read whole at the helper and at all three call sites (proven red against the previous single-read code), an over-cap body still trips each existing cap-exceeded path, reading stops mid-stream rather than buffering an oversized body, and an exact-cap body arrives complete. Closes #4829
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Real, recurring defect class; the fix reuses the repo's established streaming shape, consolidates three near-copies into one helper, and preserves every caller's cap sentinel. [DESIGN-REVIEWED] 86a8d8f |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All evidence gathered. The three buggy single- First-Principles-Verdict: PASS Every item is the declared fix for a counted defect class, the helper has three real consumers, and zero buggy siblings remain unfixed. What this change shipsIntent: stop three dashboard fetches from silently working on the first chunk of a chunked HTTP body — a FIX.
Sibling count for the root cause: grepped [FIRST-PRINCIPLES-REVIEWED] 86a8d8f |
|
🤖 Kiro Crew Auto-Pipeline [operator: iamwhatever] Review-ready at 86a8d8f: all checks green on the first push, |
|
Picking this over #4836 — two things to port first #4836 fixes the same issue (#4829) at the same three call sites ( This PR is the one to land, because it extracts one shared Two things from #4836 are worth carrying over before this merges:
Credit for both to @leonlaiyc (#4836). Approving now; holding auto-merge until those two land so they are not lost. |
bolichen97
left a comment
There was a problem hiding this comment.
Approved after a description-vs-diff consistency review: every claim in the PR description is backed by the diff, and the diff carries no material change the description leaves unmentioned.
…nk (kirodotdev#4829) (kirodotdev#4858) Three dashboard HTTP readers assumed a single StreamReader.read(cap + 1) returns the whole response body. read(n) returns UP TO n bytes, resolving as soon as any data is buffered, so on a chunked response with no Content-Length it returns only the first buffered chunk (~8-12 KiB) and the caller silently works on a truncated body: - updates._fetch_feed_bytes: release feed parsed from a partial document, so an update check can misread or fail on a chunked feed. The comment claiming an oversized body "is DETECTED rather than silently truncated" was false for a chunked feed and is corrected. - feedback._read_capped_text: a truncated Aperture response was decoded and treated as the full reply. - source_providers Jira fetch: json.loads on a partial document surfaced as SourceProviderError, so the issue read as unavailable. Same defect class as the official-registry site fixed in kirodotdev#4814. Since the single-read shape has been reintroduced independently more than once, extract one shared read_capped_response(resp, cap) helper in the dashboard handlers' _shared module (next to the request-side read_bounded_json) that drains iter_chunked chunks to EOF, enforcing the cap against the accumulated total so an oversized body is refused mid-stream, and clamping the return to cap + 1 bytes so every caller keeps its existing over-cap sentinel unchanged. All three sites route through it; cap values and caller semantics are untouched. Tests lock in: a multi-chunk body is read whole at the helper and at all three call sites (proven red against the previous single-read code), an over-cap body still trips each existing cap-exceeded path, reading stops mid-stream rather than buffering an oversized body, and an exact-cap body arrives complete. Closes kirodotdev#4829 Co-authored-by: Joe Guo <zejiangg@amazon.com>
Problem / Motivation
Three dashboard HTTP readers read a response body with a single
aiohttp.StreamReader.read(cap + 1).read(n)returns up tonbytes,resolving as soon as any data is buffered rather than waiting for EOF. On a
chunked response with no
Content-Lengthit returns only the first bufferedchunk (~8–12 KiB), so the caller silently works on a truncated body:
updates.py::_fetch_feed_bytes— the release feed is parsed from a partialdocument, so the update check can misread or fail on a chunked feed. The
site also carried a comment claiming an oversized body "is DETECTED rather
than silently truncated", which was false for a chunked feed.
feedback.py::_read_capped_text— a truncated Aperture response is decodedand treated as the full reply.
source_providers.py(Jira fetch) —json.loadson a partial Jira documentsurfaces as
SourceProviderError, so the issue reads as unavailable ratherthan a parse bug.
All three fail silently, reading as "the remote had nothing". Same defect
class as #2222 and the official-registry site fixed in #4814.
Why it matters
Update checks, feedback submission acknowledgements, and Jira issue loading
each degrade nondeterministically depending on how the remote frames its
response — with no visible error pointing at the real cause. The failure only
appears with chunked transfer encoding, which makes it environment-dependent
and expensive to diagnose.
What changed (motivation → approach → change)
Symptom: bodies truncated to the first buffered chunk. Root cause: a single
read(n)cannot deliver a streamed body; the repo's established fix shape(#4814,
official.py::_fetch_json) drainsiter_chunkedto EOF with the capenforced against the accumulated total.
Because the single-read shape has now been reintroduced independently more
than once, the fix extracts one shared helper instead of three
near-copies:
read_capped_response(resp, cap)indashboard/handlers/_shared.py, right next to the request-sideread_bounded_jsonthat already consolidated the inbound twin of thispattern. It streams to EOF, stops reading as soon as the accumulated total
exceeds the cap (an oversized body is refused mid-stream, never buffered
whole), and clamps the return to
cap + 1bytes so every caller's existingover-cap sentinel (
len(body) > cap) keeps working unchanged. All threesites route through it; cap values and caller error semantics are untouched.
The stale
updates.pycomment is corrected to describe what the code nowactually guarantees.
Tests
test/test_read_capped_response.py(new), with a fakeStreamReaderwhoseread(n)honors the real "up to n bytes" contract so a single-readimplementation is exercised and fails (all caller tests were proven red
against the pre-fix code):
capbytes arrives complete (sentinel stays off)cap + 1bytes and stops readingmid-stream (
undeliveredbytes are never consumed)feedback._read_capped_text: multi-chunk body decoded whole; over-cap bodystill raises
ValueErrorupdates._fetch_feed_bytes: chunked feed read whole; oversized feed stilltrips the caller's
len(raw) > _FEED_MAX_BYTEScheck without buffering thewhole body
json.loadscomplete andparses into the normalized issue payload
Manual verification
N/A — unit coverage sufficient: the fakes reproduce the exact chunked-stream
semantics of
aiohttp.StreamReader, and the fix is confined to the read loop.Related Issues
Closes #4829
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)