Repository navigation
[Hackathon] Bound Response Buffering (Large Histories/Steps Load Fully into Heap) #83
Description
Discord Username / User ID
uisupersaiyan3
What does this improvement do?
Summary
Every non-streaming HTTP response is fully materialized in memory via
await response.json(). For most requests this is fine, but test result --history pagination and getRun({ includeSteps: true }) can return large
payloads that are read entirely into memory before any processing or
rendering occurs.
Root Cause
The HTTP client's typed read path treats every response uniformly via
response.json(), which requires the full body to be buffered and parsed
before any part of it is available to the caller. This is appropriate for
small, bounded responses, but history and step data have no inherent upper
bound on size, so the same buffering strategy applies regardless of how
large the underlying payload is.
Affected Code
| File | Lines | Behavior |
|---|---|---|
src/lib/http.ts |
497 | response.json() used for all typed reads, with no size bound |
src/lib/http.ts |
368-383 | getRun step inclusion path, subject to the same unbounded read |
src/lib/pagination.ts |
(history pagination) | Used by test result --history, accumulates pages without a bound |
For comparison, test code get already streams its payload via bundle.ts.
The JSON read path used by history and step data does not have an
equivalent streaming or bounding mechanism.
Impact
A run with a long step array, or a test result --history invocation over a
deep history window, causes heap usage to grow in proportion to the size of
the response, with no upper bound. In the worst case, this can mean loading
an entire large history or step set into memory before printing a single
row, which is both slower to first output and more memory-intensive than
necessary.
This is a robustness concern rather than a confirmed incident: the failure
mode is degraded performance and elevated memory use under large N, not a
crash under typical usage.
Details / implementation notes
Proposed Resolution
Choose one of the following approaches for the history and step read paths:
- Bounded reads: Introduce a documented ceiling on the number of
accumulated history rows or step entries, combined with a--limitflag
so callers can explicitly request a smaller window. - Incremental rendering: Stream and render history pages as they arrive
rather than buffering the full result set before printing, so memory use
stays proportional to a single page rather than the full history.
Either approach should ensure memory use no longer scales unbounded with
response size for these two read paths.
Confirmations
- I have searched existing issues and this is not a duplicate.
- I have provided my Discord identity above for reward coordination.
Activity
zeshi-du commentedon Jul 18, 2026
Triage: parked for design (also catching up on a missed 07-03 verdict — apologies). The diagnosis is legitimate: history/steps responses buffer fully into the heap. But bounding it properly is an architectural choice (streaming JSON parse vs a hard size cap vs paginated fetch discipline) that interacts with the pagination and output layers, similar in scale to #102. We'll take it up together with those rather than piecemeal; leaving open as the tracking anchor.
Yazan-O commentedon Jul 24, 2026
/assign
testsprite-hob commentedon Jul 24, 2026
Assigned to @Yazan-O. Thanks for taking this on. (2 more slots available.)
Your open assigned issues (1/3):
(none)
zeshi-du commentedon Jul 27, 2026
@crypticsaiyan — heads-up on the Season 3 CLI Improvement Bonus coordination step: please join the TestSprite Discord and reach out to Kairui | TestSprite by 2026-07-29 23:59 UTC. Slots not claimed by then may be reallocated.
https://discord.com/invite/GXWFjCe4an
zeshi-du commentedon Aug 13, 2026
Partial fix merged, and I want to be explicit about what is and isn't done rather than let a
Closeskeyword make this look finished.Your affected-code table had three rows. Two are now fixed by #281 (merged today, thanks @Yazan-O):
src/lib/http.ts—response.json()on every typed read.HttpClient.request()now rejects a declaredContent-Lengthover the cap before touching the body, and counts bytes as they stream for chunked responses with noContent-Length.src/lib/http.ts— thegetRun/includeStepspath goes through that same choke point, so it's covered by construction.Still open, and the reason this issue stays open:
src/lib/pagination.ts— page accumulation.MAX_AUTO_PAGES = 1000and repeated-cursor detection landed via fix(paginate): bound cursor loops without dropping empty pages #48, but that bounds the number of pages, not the accumulated in-memory size.test result --historyauto-paginating over a long history still builds one array proportional to total item count, so a caller can still be surprised by heap growth even with fix(http): bound the size of buffered JSON responses #281 merged.Two shapes that would close it, roughly in order of preference:
--page-size/--cursor, consistent with themax_pages_exceededshape already there.Happy to take either. The existing typed safety errors in
pagination.tsare a good template for the error surface, and #281 is a good template for how to bound without changing the success-path contract.zeshi-du commentedon Aug 19, 2026
Closing this out.
The reported symptom —
HttpClient's typed-read path buffering an entire response into memory with no bound — is fixed. Recapping against your original three-row table:src/lib/http.ts,response.json()on every typed read (row 1) — closed by fix(http): bound the size of buffered JSON responses #281.HttpClient.request()'s typed-read path now rejects an over-cap declaredContent-Lengthbefore touching the body, and counts bytes as they stream for chunked responses that have noContent-Length, throwing a typedPAYLOAD_TOO_LARGEerror instead of growing the heap.src/lib/http.ts, thegetRun/includeStepspath (row 2) — goes through that same choke point, so it's covered by construction, not by a separate fix.src/lib/pagination.ts, page accumulation (row 3) — not closed by fix(http): bound the size of buffered JSON responses #281, and nothing else has closed it since.paginate()still collects every item from every page into oneitemsarray.MAX_AUTO_PAGES(1000) bounds the number of pages fetched before it throws a safety error — it says nothing about the accumulated size of that array.test result --historyauto-paginating over a long history still builds one array proportional to total item count, so a caller can still be surprised by heap growth even with fix(http): bound the size of buffered JSON responses #281 merged.That third row is real, and I'm not closing over it quietly — I'm closing it as a separate, narrower piece of work rather than leaving this issue open as a catch-all for it. Two shapes would close it, in order of preference:
paginationSafetyError()already uses formax_pages_exceeded— a namedreasonand structureddetails— rather than a new one-off error format. One thing worth fixing in the same pass: that existing error'snextActiontoday is a generic "retry later, contact support" string, not a pointer to a specific flag. Whichever error lands for the accumulation cap should givenextActionreal teeth — name--max-items/--page-sizedirectly — and it'd be worth tightening the existingmax_pages_exceededone to match while you're in there.If you or anyone wants to pick this up: open a fresh issue scoped to just the
pagination.tsaccumulation, link back here for context, and I'll triage it the same day. Credit where it's due — the original diagnosis and table are what made rows 1 and 2 fixable in one clean PR, and thanks @Yazan-O for #281.