{"record":{"id":"58c63f428d188ba2","repo":"Billionmail/BillionMail","slug":"download-error-d","errorCode":null,"errorMessage":"download error %d","messagePattern":"download error (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/lipsync.go","lineNumber":179,"sourceCode":"// 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\n\treturn outPath, nil\n}\n","sourceCodeStart":161,"sourceCodeEnd":195,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/lipsync.go#L161-L195","documentation":"After the GET succeeds, this fires when the video-hosting server replies with any status other than 200 OK. Unlike the submit/status calls, this branch does NOT read the response body, so the server's error detail is lost — you only get the numeric code. Common codes: 403 (signed URL expired/forbidden), 404 (video removed), 5xx (CDN/storage errors).","triggerScenarios":"resp.StatusCode != http.StatusOK on the GET of the completed lip-sync video URL — typically calling DownloadLipSyncVideo with a stale or wrong videoURL, or Sync Labs' storage returning 403/404/500.","commonSituations":"Downloading hours after the lip-sync job finished and the signed CDN URL expired; passing an empty or placeholder videoURL (empty string would fail earlier, but a malformed URL hits 404); Sync Labs outage returning 503; firewall/proxy returning a 403 block page.","solutions":["Log the actual status code (and ideally body) — 403/404 almost always mean the URL expired; re-run CheckLipSyncStatus to obtain a fresh video_url and download immediately after 'completed'.","Do not poll long then download late: fetch a new URL right before downloading instead of caching lipVideoURL.","Verify videoURL is the full https URL from resp.VideoURL, not a relative or truncated value.","If 5xx, retry — the pipeline already wraps this in withRetry; persistent 5xx means wait for the provider to recover.","Inspect any proxy/firewall in front of the host if you get unexpected 403s from your own network."],"exampleFix":"// before\nif resp.StatusCode != http.StatusOK {\n\treturn \"\", fmt.Errorf(\"download error %d\", resp.StatusCode)\n}\n// after\nif resp.StatusCode != http.StatusOK {\n\tbody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))\n\treturn \"\", fmt.Errorf(\"download error %d: %s\", resp.StatusCode, string(body))\n}","handlingStrategy":"retry","validationCode":"// verify URL freshness right before downloading\nst, err := CheckLipSyncStatus(ctx, lipCfg, lipJobID)\nif err != nil { return err }\nif st.Status != \"completed\" || st.VideoURL == \"\" {\n\treturn fmt.Errorf(\"lip sync not ready: status=%s\", st.Status)\n}\nvideoURL = st.VideoURL // use a freshly issued URL","typeGuard":"func isRetryableStatus(code int) bool {\n\treturn code == http.StatusTooManyRequests || code >= 500\n}","tryCatchPattern":"path, err := DownloadLipSyncVideo(ctx, lipCfg, videoURL, \"lipsync.mp4\")\nvar herr interface{ ... } // or check message\nif err != nil && strings.Contains(err.Error(), \"download error 40\") {\n\t// 403/404: URL expired or bad — re-fetch status, get new URL, retry once\n} else if err != nil && strings.Contains(err.Error(), \"download error 5\") {\n\t// transient server error — retry with backoff\n}","preventionTips":["Never cache videoURL across long delays; fetch a fresh one from CheckLipSyncStatus right before download","Treat 403/404 as 'refresh URL and retry', 5xx/429 as 'backoff and retry'","Instrument/log the status code so expired-vs-forbidden is distinguishable","Check Sync Labs status page if 5xx persists across retries"],"tags":["http","http-status","lipsync","expired-url"],"backgroundTag":"http-403-forbidden","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}