fix(http): bound the size of buffered JSON responses#281
Conversation
response.json() buffers the entire body before parsing with no upper bound, so a large test result --history page or a run with a long steps[] array grows the heap in proportion to the payload. Read the typed JSON response bounded by a configurable cap (HttpClientOptions.maxResponseBytes, default 64 MiB): reject an over-cap Content-Length up front, count bytes while streaming chunked bodies, and throw a typed PAYLOAD_TOO_LARGE (exit 5) with guidance to narrow --page-size / --since.
|
✅ This PR is linked to an issue assigned to @Yazan-O — thanks! The |
Walkthrough
ChangesBounded HTTP response handling
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant HttpClient
participant Response
participant readBoundedText
participant JSON.parse
HttpClient->>Response: receive successful response
HttpClient->>readBoundedText: read with maxResponseBytes
readBoundedText->>Response: stream and count body bytes
readBoundedText-->>HttpClient: bounded text or PAYLOAD_TOO_LARGE
HttpClient->>JSON.parse: parse bounded text
JSON.parse-->>HttpClient: parsed JSON or malformed response error
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/lib/http.ts (2)
971-979: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winNull-body fallback fully buffers before enforcing the cap.
When
response.bodyisnull,readBoundedTextcallsresponse.text()(fully materializing the body) and only checks its encoded length afterward — this defeats the memory-bounding goal this PR sets out to achieve for exactly the pathological/hostile-payload case it targets. It's only reachable whenContent-Lengthis absent AND no stream is exposed, but real fetch implementations rarely hit this combination for non-empty bodies, so it's a residual gap rather than the common path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/http.ts` around lines 971 - 979, Update readBoundedText’s stream === null fallback to avoid calling response.text() before enforcing maxBytes; use an incremental or otherwise bounded read mechanism that rejects once the encoded payload exceeds maxBytes while preserving the existing responseTooLargeError(requestId, maxBytes) behavior and returning the text for bodies within the cap.
989-992: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueUnguarded
reader.cancel()can mask the PAYLOAD_TOO_LARGE error.If
reader.cancel()rejects, thethrow responseTooLargeError(...)on the next line never executes; the cancel rejection propagates instead, falls through theerr instanceof ApiErrorcheck inrequestWithMeta, and gets misclassified viamalformedResponseError— silently breaking the exit-code-5 contract for this (admittedly rare) case.🛡️ Defensive fix
if (total > maxBytes) { - await reader.cancel(); + await reader.cancel().catch(() => {}); throw responseTooLargeError(requestId, maxBytes, total); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/http.ts` around lines 989 - 992, Guard the await reader.cancel() call in the response-size handling branch so any cancellation rejection is contained, then always throw responseTooLargeError(requestId, maxBytes, total). Preserve the existing oversized-payload behavior and ensure cancellation failures cannot replace the intended ApiError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/lib/http.ts`:
- Around line 971-979: Update readBoundedText’s stream === null fallback to
avoid calling response.text() before enforcing maxBytes; use an incremental or
otherwise bounded read mechanism that rejects once the encoded payload exceeds
maxBytes while preserving the existing responseTooLargeError(requestId,
maxBytes) behavior and returning the text for bodies within the cap.
- Around line 989-992: Guard the await reader.cancel() call in the response-size
handling branch so any cancellation rejection is contained, then always throw
responseTooLargeError(requestId, maxBytes, total). Preserve the existing
oversized-payload behavior and ensure cancellation failures cannot replace the
intended ApiError.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1f68da18-a6af-471f-9751-9298a900ac54
📒 Files selected for processing (2)
src/lib/http.test.tssrc/lib/http.ts
Closes #83
What
HttpClient.request()reads every successful response withawait response.json(), which buffers the entire body into memory before parsing, with no upper bound. A largetest result --historypage, or a run with a longsteps[]array (getRunwithincludeSteps), grows the heap in proportion to the payload.This adds a bounded read at that single choke point:
Content-Lengthover the cap is rejected before the body is read.Content-Length) are bounded by counting bytes as they stream, stopping once the cap is crossed.PAYLOAD_TOO_LARGEerror (exit 5 — the same code the backend already returns for oversized request bodies), with anextActionpointing at--page-size/--since.The cap is a new
HttpClientOptions.maxResponseBytes(default 64 MiB — far above any legitimate metadata/history/steps response), injectable for tests and future env/flag wiring.Scope
Focused on the unbounded typed-JSON read, which is the root cause described in #83. Pagination page-count bounding is handled separately (#248), so this does not touch
pagination.ts. Error-response bodies (small) keep using the existingsafeReadJsonpath. Happy to widen the scope if you'd prefer.The default cap value and whether to make it env-configurable are easy to adjust to your preference.
Tests (
src/lib/http.test.ts)Content-Length.Content-Length.RequestTimeoutError.npm run typecheck,npm run lint,npm run format:check, andsrc/lib/http.test.tsall pass locally.Summary by CodeRabbit
New Features
Bug Fixes