Skip to content

[Hackathon] Bound Response Buffering (Large Histories/Steps Load Fully into Heap) #83

Closed

Description

@crypticsaiyan

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 --limit flag
    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

  1. zeshi-du commented on Jul 18, 2026

    @zeshi-du
    Contributor

    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.

  2. Yazan-O commented on Jul 24, 2026

    @Yazan-O
    Contributor

    /assign

  3. added
    in-progressAssigned and actively being worked on
    and removed
    needs-triageNew issue/PR awaiting first triage
    on Jul 24, 2026
  4. testsprite-hob commented on Jul 24, 2026

    @testsprite-hob

    Assigned to @Yazan-O. Thanks for taking this on. (2 more slots available.)

    Your open assigned issues (1/3):
    (none)

  5. zeshi-du commented on Jul 27, 2026

    @zeshi-du
    Contributor

    @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

  6. added
    acceptedTriaged and accepted — a PR for this will be reviewed
    on Aug 13, 2026
  7. zeshi-du commented on Aug 13, 2026

    @zeshi-du
    Contributor

    Partial fix merged, and I want to be explicit about what is and isn't done rather than let a Closes keyword 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 declared Content-Length over the cap before touching the body, and counts bytes as they stream for chunked responses with no Content-Length.
    • ✅ src/lib/http.ts — the getRun / includeSteps path goes through that same choke point, so it's covered by construction.

    Still open, and the reason this issue stays open:

    Two shapes that would close it, roughly in order of preference:

    1. Make the accumulating callers stream — yield per page (or per item) so a caller that only renders a table never holds the whole history at once. This is the real fix; the ceiling stops mattering rather than moving.
    2. Failing that, bound accumulated bytes/items and surface a typed error naming --page-size / --cursor, consistent with the max_pages_exceeded shape already there.

    Happy to take either. The existing typed safety errors in pagination.ts are a good template for the error surface, and #281 is a good template for how to bound without changing the success-path contract.

  8. zeshi-du commented on Aug 19, 2026

    @zeshi-du
    Contributor

    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 declared Content-Length before touching the body, and counts bytes as they stream for chunked responses that have no Content-Length, throwing a typed PAYLOAD_TOO_LARGE error instead of growing the heap.
    • ✅ src/lib/http.ts, the getRun / includeSteps path (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 one items array. 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 --history auto-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:

    1. Stream instead of accumulate. Make the paginating callers yield per page (or per item) instead of collecting into one array, so a caller that's just rendering a table never holds more than one page in memory at a time. The ceiling stops mattering rather than moving.
    2. Bound the accumulation. If streaming isn't practical everywhere at once, cap accumulated items/bytes and throw a typed error when the cap is hit, following the same shape paginationSafetyError() already uses for max_pages_exceeded — a named reason and structured details — rather than a new one-off error format. One thing worth fixing in the same pass: that existing error's nextAction today is a generic "retry later, contact support" string, not a pointer to a specific flag. Whichever error lands for the accumulation cap should give nextAction real teeth — name --max-items / --page-size directly — and it'd be worth tightening the existing max_pages_exceeded one 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.ts accumulation, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

acceptedTriaged and accepted — a PR for this will be reviewedhackathonCLI hackathon submissionsin-progressAssigned and actively being worked on

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions