fix(workflow): detect oversize responses in _http_request instead of silently truncating (BE-3297)#540
fix(workflow): detect oversize responses in _http_request instead of silently truncating (BE-3297)#540mattmillerai wants to merge 3 commits into
Conversation
…uncating _http_request read a flat 64 MiB with no overflow check, so a larger body was truncated, failed to parse, and was swallowed by the JSONDecodeError handler into (status, None) — indistinguishable from an empty body. Mirror the sibling _userdata_request: read one byte past the cap, raise _ResponseTooLarge on overflow, and map it to a workflow_too_large envelope in _handle_cloud_http_error. All four call sites catch it.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 35 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Judge call failed (status=parse_error): Could not parse JSON findings from output. First 500 chars:
I've verified the code. Confirmed:
- Line 647: `body = (e.read() or b"")[:1000]...` reads the full error body before slicing, bypassing the cap.
- Lines 629–632: `json.loads(raw)` on bytes, `except json.JSONDecodeError` only — `UnicodeDecodeError` (a `ValueError`) escapes; call sites don't catch it either.
- Line 643: hint hardcoded to "saved workflow" but reused for `list`/`get`/`delete`.
- Line 1120: `save` POST can succeed server-side yet report `workflow_too_large`.
I dropped nothing beyond
Re-trigger by removing and re-adding the cursor-review label.
…r (BE-3297) Addresses the cursor-review panel findings on #540. The judge crashed on a parse error before posting, but its output named four issues; three held up. - The workflow_too_large hint was hardcoded to "the saved workflow is unexpectedly large", but _handle_cloud_http_error serves list/get/save/ delete. It was only correct for get. Replaced with a per-operation _TOO_LARGE_HINTS table. - save/delete have already sent their request by the time the response is read, so an oversize response does not mean the write was rejected. Their hints now say the change may still have landed and point at `workflow list` to confirm, rather than implying a clean failure and inviting a retry of a completed mutation. - json.loads(raw) on bytes raises UnicodeDecodeError for a non-UTF-8 body. It is a ValueError but not a JSONDecodeError, so it escaped _http_request and no call site caught it — a traceback instead of an envelope. Named it alongside JSONDecodeError. - Bounded the HTTPError body read: (e.read() or b"")[:1000] pulled an arbitrarily large error body into memory before slicing. e.read(1000) is byte-for-byte equivalent, including the fp=None case. Both non-UTF-8 tests and the hint test were confirmed to fail against the pre-fix code; the hint test fails for list/save/delete and passes for get, which is exactly the shape of the bug. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review resolution (f91cab6)The cursor-review panel's judge crashed with a Fixed1. The 2. 3. 4. The 3 and 4 are pre-existing rather than introduced here, but both sit in the two functions this PR rewrites and are squarely on its theme (don't read unbounded; don't let a malformed body produce a confusing failure), so they're fixed rather than deferred. TestsFour new tests, each confirmed to fail against the pre-fix code — the hint test fails for Deferred
On the two red checksBoth are pre-existing repo-wide breakage, unrelated to this diff (which touches only
Neither touches any file in this PR, and both pass locally. This PR's own suite is green. Note: CodeRabbit never actually reviewed this PR — it hit a Fair-Usage rate limit and posted only the limit notice, though its check reports "pass". Re-triggering below. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:
|
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Judge call failed (status=parse_error): Could not parse JSON findings from output. First 500 chars:
I've validated each finding against the actual code. Key confirmations:
- Line 642 (`except ... return status, None`): `list_cmd` (L972 `rows = (body or {}).get("data") or []`) reports `count: 0, ok: true` and `save_cmd` (L1138) emits `workflow_id: null, ok: true` when the body is undecodable. `get_cmd` (L1043) is safe (shape check), and `delete_cmd` ignores the body — so the accurate framing is list+save, not delete.
- Line 662 is where `e.read(1000)` actually lives (the reviewer's "663" points
Re-trigger by removing and re-adding the cursor-review label.
f91cab6 accidentally carried a 287-line uv.lock regeneration that has nothing to do with the HTTP oversize fix. The drift is real but pre-existing: #490 added the `bench` optional-dependency extra (anthropic>=0.40) to pyproject.toml without relocking, so main's lock has been stale since it merged — `uv lock --check` fails on main today. Any bare `uv run` in the repo silently re-locks and re-dirties the file, which is how it got picked up here. Re-locking is not this PR's job, and carrying it here invites conflicts with the dependency PRs already in flight (#533/#535/#536). No CI job consumes uv.lock, so reverting to main's copy is behavior-neutral. Filed as a follow-up instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second-pass review resolution (812274e)All review threads on this PR were already resolved by the previous pass (f91cab6). This pass found one thing that pass introduced, and re-verified the rest. Fixed: dropped an unrelated 287-line
|
Third-pass review resolution — no code changeNo unresolved review threads, no merge conflict ( The
|
ELI-5
When the CLI asks Comfy Cloud for a workflow, it reads at most 64 MiB of the reply. If the reply was bigger, the CLI quietly chopped it off. The chopped-off JSON was then broken, so parsing failed — and that failure was caught and turned into "no body at all." The user got the same result as an empty response, with no clue their workflow had been truncated.
Now the CLI reads one byte past the limit. If that extra byte exists, the body was too big, and it says so loudly with a
workflow_too_largeerror instead of pretending nothing was there.What changed
_HTTP_MAX_BYTES = 64 * 1024 * 1024, replacing the inline64 * 1024 * 1024magic number in_http_request._http_requestnow reads_HTTP_MAX_BYTES + 1and raises_ResponseTooLargewhen the body exceeds the cap — mirroring the sibling_userdata_request, which has done exactly this since it was written._handle_cloud_http_errorgained a_ResponseTooLargebranch emitting aworkflow_too_largeenvelope withdetails.limit_bytes._http_requestcall sites (list,get,save,delete) added_ResponseTooLargeto theirexcepttuple, so the new exception routes to an envelope rather than an uncaught traceback._ResponseTooLargedocstring, which named/userdataspecifically and now covers both surfaces.Judgment calls / deviations from the ticket
The ticket's premise about
_handle_cloud_http_errorwas wrong, and I fixed it rather than working around it. The ticket states the cloud handler "already knows how to map_ResponseTooLargeto alimit_bytesenvelope error." It does not — that is_handle_local_http_error, the local/userdatasibling. The cloud handler would have dropped_ResponseTooLargeinto its genericelsebranch and emitted a vaguecloud_http_error: get failed:with an empty exception string and a misleading "check network" hint. So step 3 of the plan required adding the branch, not just wiring theexcepttuples. This is the one substantive addition beyond the ticket's four steps.Separate constant rather than reusing
_USERDATA_MAX_BYTES. Both are 64 MiB and could share one name. I kept them distinct because they cap two different surfaces (local ComfyUI/userdatavs. the cloud API) that may reasonably diverge later; collapsing them would couple the two. Same value today, so no behavior difference either way.detailsincludesworkflow_idin the cloud too-large envelope (the local one omits it). It isNoneforlist/save, which is accurate and matches the other cloud error branches.Verification
uv run -p 3.10 --extra dev pytest tests/comfy_cli/command/ -q→ 1165 passed, 2 skipped.ruff format --checkandruff checkclean on both touched files.Six new tests:
TestGet::test_response_over_cap_refuses_to_truncate— end-to-end envelope assertion, includingdetails.limit_bytes.TestHttpRequestCap::test_oversize_body_raises_rather_than_returning_none— the exact regression the ticket names: oversize must raise, not return(status, None).test_body_exactly_at_cap_still_parses— boundary guard:len(raw) == capis a complete body, not a truncated one, and must still parse.test_empty_body_still_returns_none— guards the unchanged(status, None)contract for a genuinely empty body.test_every_call_site_routes_oversize_to_envelope[list|get|save|delete]— parametrized over all four call sites, so an untouchedexcepttuple fails the suite rather than surfacing a traceback to users.The two regression tests were confirmed to fail against pre-fix behavior, not just pass against the new code: reverting
_http_requestto the truncating read (resp.read(_HTTP_MAX_BYTES), no overflow check) reproducesDID NOT RAISE _ResponseTooLargeand a redtest_response_over_cap_refuses_to_truncate. The three boundary/contract tests pass in both directions by design — they guard against over-correction.On the new error path
This adds a failure path, so worth stating plainly: it does not deny a capability that works today. Fetching an oversize workflow is already broken on
main— I ran the pre-fix_http_requestagainst a truncated body and it returns(200, None), which forgetthen surfaces as a misleading "unexpected response shape" error naming the wrong cause. The change converts an already-failing silent path into a loud, correctly-coded one; it does not remove a working path. Behavior for every in-cap response (the entire real-world case) is byte-for-byte unchanged.Scope
_http_requesthas exactly four call sites, all inworkflow.py; neither it nor_ResponseTooLargeis imported anywhere else in the repo (verified by grep), so the new exception cannot escape to an unprepared caller.Deferred from a review thread on #530.