@@ -104,7 +104,11 @@ func newVideoDownloadCmd(ctx *cmdContext) *cobra.Command {
104104 "path" : dest ,
105105 })
106106 if err != nil {
107- return clierrors .New (fmt .Sprintf ("failed to encode response: %v" , err ))
107+ return & clierrors.CLIError {
108+ Code : "internal_error" ,
109+ Message : fmt .Sprintf ("failed to encode response: %v" , err ),
110+ ExitCode : clierrors .ExitGeneral ,
111+ }
108112 }
109113
110114 return ctx .formatter .Data (data , "" , nil )
@@ -122,7 +126,12 @@ func extractAssetURL(raw json.RawMessage, videoID string, info assetInfo) (strin
122126 Data map [string ]json.RawMessage `json:"data"`
123127 }
124128 if err := json .Unmarshal (raw , & resp ); err != nil {
125- return "" , clierrors .New ("failed to parse video response" )
129+ return "" , & clierrors.CLIError {
130+ Code : "response_parse_error" ,
131+ Message : "failed to parse the video response" ,
132+ Hint : "The API response could not be parsed. Retry; if it persists, report it (possible CLI/API mismatch)." ,
133+ ExitCode : clierrors .ExitGeneral ,
134+ }
126135 }
127136
128137 var status string
@@ -180,45 +189,90 @@ func assetHint(field string) string {
180189func downloadFile (ctx context.Context , videoURL , dest string ) error {
181190 req , err := http .NewRequestWithContext (ctx , http .MethodGet , videoURL , nil )
182191 if err != nil {
183- return clierrors .New (fmt .Sprintf ("failed to build download request: %v" , err ))
192+ return & clierrors.CLIError {
193+ Code : "response_parse_error" ,
194+ Message : fmt .Sprintf ("the download URL returned by the API is unusable: %v" , err ),
195+ Hint : "Re-fetch a fresh URL: heygen video get <video_id>" ,
196+ ExitCode : clierrors .ExitGeneral ,
197+ }
184198 }
185199
186200 resp , err := downloadClient .Do (req )
187201 if err != nil {
188- return clierrors .New (fmt .Sprintf ("failed to download video: %v" , err ))
202+ return & clierrors.CLIError {
203+ Code : "network_error" ,
204+ Message : fmt .Sprintf ("failed to download video: %v" , err ),
205+ Hint : "This is usually a temporary network issue. Check your connection and retry." ,
206+ ExitCode : clierrors .ExitGeneral ,
207+ }
189208 }
190209 defer resp .Body .Close ()
191210
192211 if resp .StatusCode != http .StatusOK {
193- return clierrors .New (fmt .Sprintf ("download failed with HTTP %d" , resp .StatusCode ))
212+ // A presigned asset URL that 403s/404s is expired or revoked (client can
213+ // re-fetch); any other status is the asset host (our storage/CDN) failing.
214+ if resp .StatusCode == http .StatusForbidden || resp .StatusCode == http .StatusNotFound {
215+ return & clierrors.CLIError {
216+ Code : "download_url_expired" ,
217+ Message : fmt .Sprintf ("download link expired or unavailable (HTTP %d)" , resp .StatusCode ),
218+ Hint : "This download link has expired. Re-fetch a fresh URL: heygen video get <video_id>" ,
219+ ExitCode : clierrors .ExitGeneral ,
220+ }
221+ }
222+ return & clierrors.CLIError {
223+ Code : "asset_download_failed" ,
224+ Message : fmt .Sprintf ("download failed with HTTP %d" , resp .StatusCode ),
225+ Hint : "The asset host returned an error fetching the file. This is usually transient — retry shortly." ,
226+ ExitCode : clierrors .ExitGeneral ,
227+ }
194228 }
195229
196230 // Write to a temp file in the same directory, then rename on success.
197231 // This prevents destroying an existing file on partial download failure.
198232 dir := filepath .Dir (dest )
199233 tmp , err := os .CreateTemp (dir , ".heygen-download-*.tmp" )
200234 if err != nil {
201- return clierrors .New (fmt .Sprintf ("failed to create temp file in %q: %v" , dir , err ))
235+ return & clierrors.CLIError {
236+ Code : "file_io_error" ,
237+ Message : fmt .Sprintf ("failed to create temp file in %q: %v" , dir , err ),
238+ Hint : "Could not write locally. Check the destination path and free disk space." ,
239+ ExitCode : clierrors .ExitGeneral ,
240+ }
202241 }
203242 tmpPath := tmp .Name ()
204243
205244 _ , copyErr := io .Copy (tmp , resp .Body )
206245 closeErr := tmp .Close ()
207246 if copyErr != nil {
208247 _ = os .Remove (tmpPath )
209- return clierrors .New (fmt .Sprintf ("download interrupted: %v" , copyErr ))
248+ return & clierrors.CLIError {
249+ Code : "download_interrupted" ,
250+ Message : fmt .Sprintf ("download interrupted: %v" , copyErr ),
251+ Hint : "The transfer was cut off. Retry; the partial file was cleaned up." ,
252+ ExitCode : clierrors .ExitGeneral ,
253+ }
210254 }
211255 if closeErr != nil {
212256 _ = os .Remove (tmpPath )
213- return clierrors .New (fmt .Sprintf ("failed to finalize download: %v" , closeErr ))
257+ return & clierrors.CLIError {
258+ Code : "file_io_error" ,
259+ Message : fmt .Sprintf ("failed to finalize download: %v" , closeErr ),
260+ Hint : "Could not finalize the file locally. Check free disk space." ,
261+ ExitCode : clierrors .ExitGeneral ,
262+ }
214263 }
215264
216265 // Atomic rename. On Windows this may fail if dest is open elsewhere;
217266 // os.Rename across filesystems also fails, but temp file is in the
218267 // same directory so this is safe.
219268 if err := os .Rename (tmpPath , dest ); err != nil {
220269 _ = os .Remove (tmpPath )
221- return clierrors .New (fmt .Sprintf ("failed to move download to %q: %v" , dest , err ))
270+ return & clierrors.CLIError {
271+ Code : "file_io_error" ,
272+ Message : fmt .Sprintf ("failed to move download to %q: %v" , dest , err ),
273+ Hint : "Could not write to the destination path. Check permissions and free disk space." ,
274+ ExitCode : clierrors .ExitGeneral ,
275+ }
222276 }
223277
224278 return nil
0 commit comments