{"record":{"id":"548bc658aac50d19","repo":"Billionmail/BillionMail","slug":"lipsync-status-api-error-d-s","errorCode":null,"errorMessage":"lipsync status API error %d: %s","messagePattern":"lipsync status API error (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/lipsync.go","lineNumber":151,"sourceCode":"}\n\n// CheckLipSyncStatus checks the status of a lip sync job.\nfunc CheckLipSyncStatus(ctx context.Context, cfg LipSyncConfig, jobID string) (*LipSyncResponse, error) {\n\thttpReq, err := BuildLipSyncStatusRequest(cfg, jobID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thttpReq = httpReq.WithContext(ctx)\n\n\tresp, err := cfg.doHTTP(httpReq)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"lipsync status API call: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"lipsync status API error %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar result LipSyncResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\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)","sourceCodeStart":133,"sourceCodeEnd":169,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/lipsync.go#L133-L169","documentation":"CheckLipSyncStatus treats any non-200 response from the Sync Labs status endpoint as an error, embedding the status code and the raw response body. This surfaces API-side rejections: invalid or missing x-api-key (401/403), unknown job ID (404), rate limiting (429), or server errors (5xx). The body text is included so the upstream message (e.g. 'job not found') is visible.","triggerScenarios":"Calling CheckLipSyncStatus with a jobID that does not exist or has expired, an empty/wrong SYNCLABS_API_KEY, a BaseURL override that routes to the wrong path, or hitting Sync Labs rate limits while polling many jobs in a tight loop.","commonSituations":"Stale jobID reused after the job record was purged server-side; API key rotated or set to a key from another Sync Labs account; polling loop without backoff triggering 429; typo'd BaseURL producing 404 HTML that gets embedded in the message.","solutions":["Check the status code and body in the error: 401/403 → fix SYNCLABS_API_KEY; 404 → verify the jobID came from a successful SubmitLipSync; 429 → add polling backoff; 5xx → retry later.","Confirm the API key env var is set and valid before polling.","Only poll job IDs returned by SubmitLipSync in the same run; persist them with their creation time if polling across restarts.","If BaseURL is overridden, verify the URL matches the Sync Labs path layout (/lipsync/{id})."],"exampleFix":"// before: polls immediately and reuses an unpersisted jobID\nstatus, err := video_gen.CheckLipSyncStatus(ctx, cfg, oldJobID)\n// after: poll a fresh jobID with exponential backoff on 429/5xx\nfor {\n    status, err := video_gen.CheckLipSyncStatus(ctx, cfg, jobID)\n    if err != nil {\n        if isRetryableStatusErr(err) { time.Sleep(backoff); backoff *= 2; continue }\n        return err\n    }\n    break\n}","handlingStrategy":"fallback","validationCode":"if cfg.APIKey == \"\" { return errors.New(\"SYNCLABS_API_KEY not set\") }\nif jobID == \"\" { return errors.New(\"empty lipsync jobID\") }","typeGuard":"type statusAPIError struct{ Code int; Body string }\nfunc asStatusAPIError(err error) (code int, body string, ok bool) {\n    m := regexp.MustCompile(`lipsync status API error (\\d+): `).FindStringSubmatch(err.Error())\n    if m == nil { return 0, \"\", false }\n    code, _ = strconv.Atoi(m[1])\n    return code, strings.TrimPrefix(err.Error(), m[0]), true\n}","tryCatchPattern":"status, err := video_gen.CheckLipSyncStatus(ctx, cfg, jobID)\nif err != nil {\n    if code, body, ok := asStatusAPIError(err); ok {\n        switch {\n        case code == http.StatusUnauthorized || code == http.StatusForbidden:\n            return fmt.Errorf(\"check SYNCLABS_API_KEY: %s\", body)\n        case code == http.StatusNotFound:\n            return fmt.Errorf(\"lipsync job %s not found; resubmit\", jobID)\n        case code == http.StatusTooManyRequests || code >= 500:\n            // back off and retry\n        }\n    }\n    return err\n}","preventionTips":["Verify the API key at service startup, not per-poll.","Only poll job IDs returned by SubmitLipSync; persist them if polling spans restarts.","Add exponential backoff between polls to avoid 429s.","Never reuse jobIDs from previous API versions or purged jobs."],"tags":["http-status","api-error","lipsync","auth"],"backgroundTag":"http-4xx-api-error","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"}