cli: classify video-download failures into specific codes#207
Conversation
0451ea0 to
78cbc7d
Compare
407e491 to
10571a8
Compare
78cbc7d to
26f1223
Compare
10571a8 to
e15db1e
Compare
26f1223 to
a010fc9
Compare
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at c42c4de.
Traced every error-emission site in downloadFile + extractAssetURL + RunE, cross-checked the test file, and re-read errors.go for how these codes flow into the envelope. The classification is sound, exit-code preservation holds (every new path is ExitGeneral / 1), the presigned-URL 403/404 split is a real usability win, and cli_download_failed vs the API's download_failed is a well-motivated distinction. Two things I'd like to see addressed before this lands, plus a coverage note.
Concerns
-
🟠 PR body ↔ code drift. The "How it works" table in the PR description lists the codes as
download_url_expired,asset_download_failed,download_interrupted,file_io_error,response_parse_error,internal_error. The actual codes in the diff are:PR body Actual code download_url_expiredcli_download_url_expiredasset_download_failedcli_download_failednetwork_errornetwork_error(no prefix)download_interruptedcli_download_interruptedfile_io_errorcli_file_io_errorresponse_parse_errorcli_response_parse_errorinternal_errorcli_response_encode_error(different name)The last commit (c42c4de — "namespace CLI-originated download/encode error codes with
cli_prefix") clearly happened after the body was written. Two consumer-facing consequences: (1) the "Follow-up (not in this PR)" note about the dashboard server-list references the old names, and someone updating the server-list from the PR description will register the wrong strings; (2) the design-decision paragraph ("asset_download_failedis intentionally CLI-specific, not the API'sdownload_failed") still argues forasset_download_failedeven though the ship name iscli_download_failed. The rationale still applies, just under a different name. Please refresh the PR body table + follow-up section so it matches the codes users will actually see. -
🟠 Prefix inconsistency inside
downloadFile. Six of the new codes get thecli_prefix;network_error(video_download.go:204) does not. The reason is presumably thatnetwork_erroris a pre-existing shared code (already emitted byinternal/client/executor.go:205for API transport failures) and you didn't want to fork the name. That's a reasonable call, but it isn't spelled out anywhere — a future contributor adding another CLI-side code will have no rule to follow. Consider one of:- Add a short comment at the top of
downloadFile(or above the firstcli_*emission) documenting: "CLI-originated failure codes here are prefixedcli_to disambiguate from API codes;network_erroris intentionally unprefixed because it's shared with the API-executor transport-failure path." - Or bite the bullet and make
network_error→cli_network_errorand rev the executor too (larger blast radius, probably not worth it in this PR).
Either is fine; leaving the rule undocumented isn't.
- Add a short comment at the top of
-
🟡 Failure-path test coverage. Nice coverage on
cli_download_failed(500),cli_download_url_expired(403/404), and bothcli_response_parse_errorpaths (malformed JSON + unusable URL). Three of the seven new codes are still untested:network_error(transport error):downloadClient.Dofailure — testable by pointingdownloadClientat a listener that refuses connect, or by cancelling context mid-request.cli_download_interrupted(mid-streamio.Copycutoff): testable with a handler that writesContent-Length: Nthen closes after < N bytes, or that hijacks the connection and drops.cli_file_io_error(temp-create / rename / close): the tempfile-create branch is testable by pointing--output-pathat a directory whose parent isn't writable (or non-existent).
Per feedback_observability_pr_failure_path_coverage — the untested dispatch branches are exactly the ones that regress silently on refactors. At least covering
network_errorandcli_download_interruptedwould lock the classification in.cli_response_encode_error(json.Marshalofmap[string]string) is effectively unreachable and not worth testing.
Nits
-
cli_response_parse_errordoing double duty (video_download.go:131and:194). The first is a JSON.Unmarshal failure on the API response body; the second ishttp.NewRequestWithContextfailing on an unparseable URL. Both are "the CLI got something it can't use from the API," so the shared code is defensible, but they're conceptually distinct — a URL parse failure isn't really a "response parse" thing. If you split,cli_bad_download_urlfor the URL case would be clearer. Not blocking. (nit) -
cli_download_interruptedhint — "The transfer was cut off. Retry; the partial file was cleaned up." Small phrasing thing: since retry advice is baked into the hint, worth ending with a period-then-sentence rather than semicolons, matching the other hints' style. (nit)
Questions
- Is the follow-up TODO ("Dashboard server-list at release: add
asset_download_failed+response_parse_error; move the API's client-sidedownload_failed(400) out of the server-list") tracked anywhere — Linear,TODOcomment, checklist? Not blocking for this PR (the CLI is self-contained), but the API'sdownload_failedcontinuing to live in a "server-side" server-list is exactly the kind of thing that gets forgotten across a release.
What I didn't verify
- Whether Graphite will cleanly rebase #207 onto
mainafter #206 lands (base is07-07-cli_error_docurl_param). No conflicting files but there's always drift risk — see feedback_graphite_restack_silent_revert if you notice anything odd post-merge. - End-to-end: didn't drive
heygen video downloadagainst a real broken CDN to confirm the codes emit as expected. - Whether the CI govulncheck failure is pre-existing (I checked briefly — it's
golang.org/x/crypto@v0.52.0reachable viaselfupdate.init, unrelated to this diff).
e15db1e to
844c843
Compare
c42c4de to
08254f1
Compare
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
R2 — reviewed at 08254f1.
The restack picked up the executor-test fixture fixes from #206, but the PR-207-specific R1 items are unaddressed. Full diff since R1 (c42c4de..08254f1) is a 4-line propagation from #206 — no changes to cmd/heygen/video_download.go, no new tests, no doc updates.
Resolution
- 🟠 #3 PR body ↔ code drift — ❌ Not addressed, and worse than R1. Body table still shows pre-rename codes for all seven rows. Two are not just missing the
cli_prefix — they were renamed:- Body:
asset_download_failed→ code:cli_download_failed(cmd/heygen/video_download.go:224). The clarifying "asset" is gone;cli_download_failedis now one prefix step from the API's owndownload_failed(400) — the exact collision the body's Design-decisions paragraph explicitly warns against. - Body:
internal_error→ code:cli_response_encode_error(cmd/heygen/video_download.go:108). Different name entirely. - The other four (
download_url_expired,download_interrupted,file_io_error,response_parse_error) are missing thecli_prefix. - Both prose sections — "Design decisions" and the "Follow-up (dashboard server-list)" — still say
asset_download_failed. If someone reads the body and registers that string in the dashboard, they'll register a name the CLI no longer emits.
- Body:
- 🟠 #4
network_errorunprefixed rationale — ❌ Not addressed. Grep-verified the R1 hypothesis: name is shared acrossinternal/client/executor.go:205andcmd/heygen/video_download.go:204. The asymmetry is deliberate but undocumented; still needs a code comment at one of the two emission sites so the next person doesn't "fix" the inconsistency and break executor. - 🟡 #5 Coverage gap — ❌ Not addressed. No new tests since R1.
network_error,cli_download_interrupted,cli_file_io_errorremain untested.httptesthandles two of the three (transport failure by hanging up the connection; chunked-response abort mid-stream); ~15–20 lines total. - #6 Clean items — remain clean.
os.Remove(tmpPath)fires on all three write-path failures (video_download.go:248,257,270); every emission stillclierrors.ExitGeneral; no callers of the old un-prefixed names remain in non-test code.
CI
Same pre-existing govulncheck failure as #206 — golang.org/x/crypto@v0.52.0 via selfupdate.init, not a regression.
Holding on the same #3 + #4 items from R1. The PR-body update is the load-bearing one — the "Follow-up" section explicitly names strings a downstream consumer would use to register codes.
08254f1 to
87b9f59
Compare
|
R2 items addressed (branch amended + restacked; SHAs updated, CI re-running): #3 PR body ↔ code drift — fixed. Body table now shows the actual shipped codes ( #4 #5 Coverage gap — closed. Added three Nits:
Follow-up question — yes, the dashboard server-list work (and the
|
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
R3 — reviewed at 87b9f59.
Force-push landed the R2 fixes. Verifying each prior item independently against HEAD:
| R2 finding | Status | Evidence |
|---|---|---|
🟠 PR body ↔ code drift (7 pre-rename codes, asset_download_failed in follow-up, Design-decisions warning under old name) |
✅ Addressed | Body table now enumerates the actually-shipped codes; Design-decisions paragraph argues under cli_download_failed and pins the API-download_failed (400) collision to the cli_ prefix; Follow-up section uses new names and references PRINFRA-247. |
🟠 network_error unprefix rationale undocumented |
✅ Mostly addressed (see below) | 4-line comment at cmd/heygen/video_download.go:203-206 explains why it stays bare and points at the sibling emission in internal/client/executor.go. |
🟡 Coverage gap — network_error, cli_download_interrupted, cli_file_io_error untested |
✅ Addressed | Three new httptest-backed tests in cmd/heygen/video_download_test.go: TestVideoDownload_NetworkError (dead listener → transport failure at 523-549), TestVideoDownload_Interrupted (hijack + Content-Length overshoot then drop → mid-stream EOF at 552-591), TestVideoDownload_FileIOError (dest under missing parent → temp-create fails at 594-620). All three assert the specific code in the stderr envelope. Ubuntu / macOS / Windows test jobs all green on this run. |
Nit: cli_download_interrupted hint sentence style |
✅ Addressed | Reworded to period-separated ("Retry the download. The partial file was cleaned up.") at video_download.go:255. |
New concern from R2 fix — 🟡 network_error documentation is one-sided and points at a file that doesn't exist
The R2 fix landed at the video-download emission site, but two smaller gaps remain around it:
-
Broken cross-reference. The new comment at
cmd/heygen/video_download.go:206says "See the grandfathered set in internal/errors/codes.go before 'fixing' this." — butinternal/errors/codes.godoes not exist (find internal/errors -name '*.go'yields onlyapi_error.go,errors.go,errors_test.go). The Design-decisions paragraph in the PR body makes the same claim ("documented at the emission site and ininternal/errors/codes.go"). A future contributor who follows the pointer will hit a dead end. Either drop thecodes.goclause from both the comment and the PR body, or actually add acodes.go(or a// Grandfathered:marker inerrors.go) that enumerates the bare-code set. -
Sibling site has no matching comment. The R2 concern was that a future contributor would drift the two sites — and while the video-download site now warns them, the executor site (
internal/client/executor.go:205) is untouched:return nil, &clierrors.CLIError{ Code: "network_error", Message: fmt.Sprintf("request failed: %v", err), ...
A contributor doing "rename bare codes to
client_prefix" inexecutor.gosees no signal at the site they're actually editing. A short mirror comment there ("see cmd/heygen/video_download.go for the shared-code rationale") closes the loop.
Non-blocking — the core R2 ask (someone reading the video-download emission site understands why it's bare) is satisfied. But the "grandfathered set" mechanism the comment promises isn't real yet, and the sibling site is still exposed.
What I verified
- Somansh's summary comment claims independently verified against the diff — all three R2 items land as described (
gh api repos/heygen-com/heygen-cli/pulls/207/commits+ fullpr diff). - The three new tests structurally exercise the code paths they claim (dead-listener transport error,
Content-Lengthovershoot with hijack + close,os.CreateTempin a missing parent dir). cli_response_encode_errorremains untested — acknowledged in the PR body as ajson.Marshalof a fixed map (effectively unreachable), which matches my R2 note.- No competing peer reviews to layer against — I'm the only reviewer on this PR.
- CI: tests / lint / secrets / goreleaser / pr-template all green.
govulncheckred is pre-existingx/crypto@v0.52.0-via-selfupdate.initplus a fresh Go-stdlib TLS advisory (GO-2026-5856,crypto/tls@go1.25.11) on files this PR doesn't touch — environmental, not a regression. - Mergeable:
MERGEABLE.
What I didn't verify
- Whether
cli_download_url_expiredfires for a 401 or other 4xx on a presigned URL — the code only branches on 403/404 and lumps 401 intocli_download_failed. If AWS/GCP presigned URLs ever return 401 for signature-expiry, that error would misclassify as "asset host failing." Non-blocker unless the storage provider's semantics say otherwise; worth a follow-up once you see production data.
LGTM from my side pending your call on the 🟡 above. Leaving as a comment (approval routing lives elsewhere).
87b9f59 to
2291775
Compare
|
R3 🟡 fixed. The
Net: both |
jrusso1020
left a comment
There was a problem hiding this comment.
Approving at 2291775f. Download-failure classification is sound: 403/404 → cli_download_url_expired (presigned URL expired, re-fetchable) vs other HTTP → cli_download_failed (asset-host failure), plus cli_file_io_error / cli_download_interrupted / cli_response_parse/encode_error. Confirmed the R3 nit is resolved — the comment no longer references a nonexistent internal/errors/codes.go; it now reads "grandfathered shared transport code, intentionally unprefixed," and network_error stays bare with a clear rationale (shared with executor.go transport path, don't prefix one site without the other). New httptest-backed tests cover the gap codes. All required checks green; govulncheck red is the same non-required pre-existing x/crypto finding. LGTM.
Replace the generic `error` on every download-path failure with specific codes: response_parse_error (unparseable video-get response / unusable URL), network_error (transport), download_url_expired (403/404 on the presigned asset URL), asset_download_failed (asset-host 5xx/other), download_interrupted (mid-stream), file_io_error (local temp/finalize/rename), internal_error (encoding our own output). Each carries an actionable hint. No exit-code changes. asset_download_failed is intentionally CLI-specific: the API's own `download_failed` is a 400 for a bad user-provided input URL (opposite meaning), so reusing it would collide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…refix Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2291775 to
5d905a5
Compare
Scope
Surfaces: CLI (video download) | Module: Error handling
Stacked on #206.
Summary
The video-download path collapsed every failure — expired link, CDN error, network drop, local disk error, unparseable response — into the generic
errorcode. This splits each into a specific, actionable code so an agent or user knows what actually failed and what to do. CLI-originated codes carry the reservedcli_prefix.How it works
downloadFilefetches the finished asset directly from a presigned CDN URL (the v3 API is not in that path), so these are all CLI-side failures classified by where they occur:cli_download_url_expiredheygen video getcli_download_failednetwork_errorio.Copy)cli_download_interruptedcli_file_io_errorcli_response_parse_errorcli_response_encode_errorEach carries a matching hint. No exit-code changes — all were exit 1 and stay exit 1.
Design decisions
CLI-originated codes are
cli_-prefixed. These are all CLI-side failures (the CLI fetches the asset from a presigned CDN URL; the API is not in the path), so their codes carry the reservedcli_prefix to keep them out of the API's namespace and collision-proof against any current or future API code.network_erroris the one deliberate exception: it is a grandfathered, released code shared with the API-executor transport path (internal/client/executor.go), kept bare so both sites agree (documented at the emission site).cli_download_failedvs the API'sdownload_failed. The API'sdownload_failedis a 400 for a user-provided input URL the API failed to fetch (a client error) — the opposite of the CLI failing to fetch the finished asset from the CDN (server-side). These near-opposite conditions must not share a code; thecli_prefix keeps them unambiguous. (An earlierasset_-qualified name dodged the same collision by hand; the prefix now handles it structurally.)Split by origin, not lumped.
cli_download_url_expired(client can fix by re-fetching) is separated fromcli_download_failed(our storage failing) because the remedies differ.Testing
httptest-backed download tests: 403/404 →cli_download_url_expired(+ re-fetch hint), 500 →cli_download_failed, malformed JSON + unusable URL →cli_response_parse_error, transport failure →network_error, mid-stream cutoff →cli_download_interrupted, missing-parent-dir →cli_file_io_error. Existing download success/not-ready tests still pass.make lintclean. (cli_response_encode_erroris ajson.Marshalof a fixed map — effectively unreachable, not tested.)Follow-up (not in this PR) — tracked in PRINFRA-247
Dashboard server-list at release: add
cli_download_failed+cli_response_parse_error; move the API's client-sidedownload_failed(400) out of the server-list. Documenting thecli_codes and wiring theirdoc_urlis tracked in the same issue.sourceorigin tag is in telemetry (#208).