@@ -104,7 +104,12 @@ 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+ Hint : "Please report this CLI bug with the command you ran." ,
111+ ExitCode : clierrors .ExitGeneral ,
112+ }
108113 }
109114
110115 return ctx .formatter .Data (data , "" , nil )
@@ -122,7 +127,12 @@ func extractAssetURL(raw json.RawMessage, videoID string, info assetInfo) (strin
122127 Data map [string ]json.RawMessage `json:"data"`
123128 }
124129 if err := json .Unmarshal (raw , & resp ); err != nil {
125- return "" , clierrors .New ("failed to parse video response" )
130+ return "" , & clierrors.CLIError {
131+ Code : "response_parse_error" ,
132+ Message : "failed to parse the video response" ,
133+ Hint : "The API response could not be parsed. Retry; if it persists, report it (possible CLI/API mismatch)." ,
134+ ExitCode : clierrors .ExitGeneral ,
135+ }
126136 }
127137
128138 var status string
@@ -180,45 +190,90 @@ func assetHint(field string) string {
180190func downloadFile (ctx context.Context , videoURL , dest string ) error {
181191 req , err := http .NewRequestWithContext (ctx , http .MethodGet , videoURL , nil )
182192 if err != nil {
183- return clierrors .New (fmt .Sprintf ("failed to build download request: %v" , err ))
193+ return & clierrors.CLIError {
194+ Code : "response_parse_error" ,
195+ Message : fmt .Sprintf ("the download URL returned by the API is unusable: %v" , err ),
196+ Hint : "Re-fetch a fresh URL: heygen video get <video_id>" ,
197+ ExitCode : clierrors .ExitGeneral ,
198+ }
184199 }
185200
186201 resp , err := downloadClient .Do (req )
187202 if err != nil {
188- return clierrors .New (fmt .Sprintf ("failed to download video: %v" , err ))
203+ return & clierrors.CLIError {
204+ Code : "network_error" ,
205+ Message : fmt .Sprintf ("failed to download video: %v" , err ),
206+ Hint : "This is usually a temporary network issue. Check your connection and retry." ,
207+ ExitCode : clierrors .ExitGeneral ,
208+ }
189209 }
190210 defer resp .Body .Close ()
191211
192212 if resp .StatusCode != http .StatusOK {
193- return clierrors .New (fmt .Sprintf ("download failed with HTTP %d" , resp .StatusCode ))
213+ // A presigned asset URL that 403s/404s is expired or revoked (client can
214+ // re-fetch); any other status is the asset host (our storage/CDN) failing.
215+ if resp .StatusCode == http .StatusForbidden || resp .StatusCode == http .StatusNotFound {
216+ return & clierrors.CLIError {
217+ Code : "download_url_expired" ,
218+ Message : fmt .Sprintf ("download link expired or unavailable (HTTP %d)" , resp .StatusCode ),
219+ Hint : "This download link has expired. Re-fetch a fresh URL: heygen video get <video_id>" ,
220+ ExitCode : clierrors .ExitGeneral ,
221+ }
222+ }
223+ return & clierrors.CLIError {
224+ Code : "asset_download_failed" ,
225+ Message : fmt .Sprintf ("download failed with HTTP %d" , resp .StatusCode ),
226+ Hint : "The asset host returned an error fetching the file. This is usually transient — retry shortly." ,
227+ ExitCode : clierrors .ExitGeneral ,
228+ }
194229 }
195230
196231 // Write to a temp file in the same directory, then rename on success.
197232 // This prevents destroying an existing file on partial download failure.
198233 dir := filepath .Dir (dest )
199234 tmp , err := os .CreateTemp (dir , ".heygen-download-*.tmp" )
200235 if err != nil {
201- return clierrors .New (fmt .Sprintf ("failed to create temp file in %q: %v" , dir , err ))
236+ return & clierrors.CLIError {
237+ Code : "file_io_error" ,
238+ Message : fmt .Sprintf ("failed to create temp file in %q: %v" , dir , err ),
239+ Hint : "Could not write locally. Check the destination path and free disk space." ,
240+ ExitCode : clierrors .ExitGeneral ,
241+ }
202242 }
203243 tmpPath := tmp .Name ()
204244
205245 _ , copyErr := io .Copy (tmp , resp .Body )
206246 closeErr := tmp .Close ()
207247 if copyErr != nil {
208248 _ = os .Remove (tmpPath )
209- return clierrors .New (fmt .Sprintf ("download interrupted: %v" , copyErr ))
249+ return & clierrors.CLIError {
250+ Code : "download_interrupted" ,
251+ Message : fmt .Sprintf ("download interrupted: %v" , copyErr ),
252+ Hint : "The transfer was cut off. Retry; the partial file was cleaned up." ,
253+ ExitCode : clierrors .ExitGeneral ,
254+ }
210255 }
211256 if closeErr != nil {
212257 _ = os .Remove (tmpPath )
213- return clierrors .New (fmt .Sprintf ("failed to finalize download: %v" , closeErr ))
258+ return & clierrors.CLIError {
259+ Code : "file_io_error" ,
260+ Message : fmt .Sprintf ("failed to finalize download: %v" , closeErr ),
261+ Hint : "Could not finalize the file locally. Check free disk space." ,
262+ ExitCode : clierrors .ExitGeneral ,
263+ }
214264 }
215265
216266 // Atomic rename. On Windows this may fail if dest is open elsewhere;
217267 // os.Rename across filesystems also fails, but temp file is in the
218268 // same directory so this is safe.
219269 if err := os .Rename (tmpPath , dest ); err != nil {
220270 _ = os .Remove (tmpPath )
221- return clierrors .New (fmt .Sprintf ("failed to move download to %q: %v" , dest , err ))
271+ return & clierrors.CLIError {
272+ Code : "file_io_error" ,
273+ Message : fmt .Sprintf ("failed to move download to %q: %v" , dest , err ),
274+ Hint : "Could not write to the destination path. Check permissions and free disk space." ,
275+ ExitCode : clierrors .ExitGeneral ,
276+ }
222277 }
223278
224279 return nil
0 commit comments