{"record":{"id":"982dc4064216811e","repo":"Billionmail/BillionMail","slug":"download-lipsync-video-w","errorCode":null,"errorMessage":"download lipsync video: %w","messagePattern":"download lipsync video: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/lipsync.go","lineNumber":174,"sourceCode":"\t\treturn nil, fmt.Errorf(\"decode lipsync status: %w\", err)\n\t}\n\treturn &result, nil\n}\n\n// DownloadLipSyncVideo downloads the completed lip sync video to the output directory.\nfunc DownloadLipSyncVideo(ctx context.Context, cfg LipSyncConfig, videoURL, filename string) (string, error) {\n\tif err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {\n\t\treturn \"\", fmt.Errorf(\"create output dir: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", videoURL, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"create download request: %w\", err)\n\t}\n\n\tresp, err := cfg.doHTTP(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"download lipsync video: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"download error %d\", resp.StatusCode)\n\t}\n\n\toutPath := filepath.Join(cfg.OutputDir, filename)\n\tf, err := os.Create(outPath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"create output file: %w\", err)\n\t}\n\tdefer f.Close()\n\n\tif _, err := io.Copy(f, resp.Body); err != nil {\n\t\treturn \"\", fmt.Errorf(\"write video data: %w\", err)\n\t}\n","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/lipsync.go#L156-L192","documentation":"DownloadLipSyncVideo fetches the finished lip-sync video from the Sync Labs CDN URL via HTTP GET. This error is returned when the HTTP transport itself fails — the request never produced a response (DNS failure, connection refused/reset, TLS error, or the request context was canceled/timed out). It wraps the underlying net/http error with %w so errors.Is/As (e.g. context.DeadlineExceeded) still work.","triggerScenarios":"cfg.doHTTP(req) returns a non-nil error for the GET of videoURL: network outage, unresolvable CDN hostname, the rate-limited RateLimitedClient exhausting its wait window, or ctx already canceled/expired before the download.","commonSituations":"Container has no outbound internet or broken DNS; Sync Labs signed video URLs have expired by the time the download happens after the 5-minute lipSyncTimeout poll; a corporate proxy/TLS MITM breaks the connection; pipeline ctx deadline expires during a long download; the rate-limited client (60s max wait) times out under burst load.","solutions":["Check outbound connectivity and DNS from the host (curl the videoURL) — the error text wraps the root cause, read the chained message.","Verify the videoURL is fresh: re-poll CheckLipSyncStatus to get a new signed URL if the old one expired before downloading.","Increase the caller's context deadline so long downloads are not cut off, and confirm ctx is not already canceled.","Confirm the RateLimitedClient 'synclabs' burst/limit settings aren't causing request-drops; raise maxWait if so.","Retry: callers already wrap this in withRetry (3 attempts, exponential backoff) — for persistent failures fix the network path, not the retry count."],"exampleFix":"// before\nlipVideoURL, lipErr := pollLipSync(ctx, lipCfg, lipJobID)\ndlPath, lipErr := DownloadLipSyncVideo(ctx, lipCfg, lipVideoURL, \"lipsync.mp4\")\n// after\n// use a fresh context with an explicit download deadline instead of the long-lived pipeline ctx\nctxDl, cancel := context.WithTimeout(context.Background(), 2*time.Minute)\ndefer cancel()\ndlPath, lipErr := DownloadLipSyncVideo(ctxDl, lipCfg, lipVideoURL, \"lipsync.mp4\")","handlingStrategy":"retry","validationCode":"// before calling\nu, err := url.Parse(videoURL)\nif err != nil || u.Scheme != \"https\" || u.Host == \"\" {\n\treturn fmt.Errorf(\"invalid video URL: %q\", videoURL)\n}\nif ctx.Err() != nil {\n\treturn ctx.Err() // context already canceled\n}","typeGuard":"func isTransportError(err error) bool {\n\tvar ne net.Error\n\tif errors.As(err, &ne) { return true }\n\treturn errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) || errors.Is(err, io.ErrUnexpectedEOF)\n}","tryCatchPattern":"path, err := DownloadLipSyncVideo(ctx, lipCfg, lipVideoURL, \"lipsync.mp4\")\nif err != nil {\n\tif isTransportError(err) {\n\t\t// transient: retry with backoff or re-fetch a fresh URL via CheckLipSyncStatus\n\t}\n\treturn fmt.Errorf(\"download lipsync video: %w\", err)\n}","preventionTips":["Always download immediately after status becomes 'completed' — signed CDN URLs expire","Give the download its own generous context timeout separate from polling","Use the rate-limited client with a wait window larger than the expected download time","Verify outbound network/DNS in the deployment environment before enabling the pipeline"],"tags":["network","http","lipsync","download"],"backgroundTag":"http-request-failed","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}