{"record":{"id":"cfdb9236a72d67b6","repo":"Billionmail/BillionMail","slug":"lipsync-status-api-call-w","errorCode":null,"errorMessage":"lipsync status API call: %w","messagePattern":"lipsync status API call: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/lipsync.go","lineNumber":145,"sourceCode":"\n\tvar result LipSyncResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn \"\", fmt.Errorf(\"decode lipsync response: %w\", err)\n\t}\n\treturn result.ID, nil\n}\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 {","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/lipsync.go#L127-L163","documentation":"CheckLipSyncStatus polls the Sync Labs lipsync API (GET {base}/lipsync/{jobID}) to fetch a job's status. This error wraps a transport-level failure returned by the configured HTTP client (RateLimitedClient or http.DefaultClient) before any status code can be inspected. It means the request never completed successfully at the HTTP level — DNS failure, connection refused/reset, TLS error, timeout, or context cancellation.","triggerScenarios":"Calling CheckLipSyncStatus(ctx, cfg, jobID) when cfg.doHTTP returns a non-nil error: network outage, unreachable api.synclabs.so, misconfigured cfg.BaseURL (e.g. wrong port or scheme for a test stub), an expired/canceled ctx, or the RateLimitedClient giving up after exhausting retries.","commonSituations":"No internet/DNS in the container; SYNCLABS_API_KEY environment points at a mocked BaseURL that is not running; job polling outlives a short-lived HTTP request context; rate-limited client aborts during a traffic spike.","solutions":["Verify network connectivity to the lipsync API host (curl the base URL) and fix DNS/proxy/firewall issues.","If BaseURL is overridden for testing, confirm the mock server is running and the URL scheme/host/port are correct.","Check whether the passed ctx was canceled or expired; give polling a context with an adequate deadline.","Inspect the wrapped cause (%w) with errors.Is/As for net.Error, context.DeadlineExceeded, or rate-limiter exhaustion and apply the matching fix (retry with backoff, longer timeout)."],"exampleFix":"// before: pollLipSync uses a bare ctx that may expire mid-poll\nstatus, err := video_gen.CheckLipSyncStatus(ctx, cfg, jobID)\n// after: bound polling with an explicit deadline and classify the error\npollCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)\ndefer cancel()\nstatus, err := video_gen.CheckLipSyncStatus(pollCtx, cfg, jobID)\nif err != nil {\n    if errors.Is(err, context.DeadlineExceeded) || isNetError(err) {\n        // retry with backoff\n    }\n}","handlingStrategy":"retry","validationCode":"// pre-flight reachability check\nif cfg.BaseURL != \"\" {\n    u, err := url.Parse(cfg.BaseURL)\n    if err != nil || u.Host == \"\" { return fmt.Errorf(\"bad lipsync BaseURL: %w\", err) }\n}\nif err := ctx.Err(); err != nil { return err } // context already dead","typeGuard":"func isNetError(err error) bool {\n    var ne net.Error\n    if errors.As(err, &ne) { return true }\n    return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) || errors.Is(err, syscall.ECONNREFUSED)\n}","tryCatchPattern":"status, err := video_gen.CheckLipSyncStatus(ctx, cfg, jobID)\nif err != nil {\n    if isNetError(err) {\n        // transient: retry with exponential backoff up to N times\n    }\n    return fmt.Errorf(\"poll lipsync job %s: %w\", jobID, err)\n}","preventionTips":["Set a generous but bounded context deadline for the whole poll loop.","Configure the RateLimitedClient with sane timeout and retry policy for polling traffic.","Smoke-test BaseURL overrides with a health check before running jobs.","Use errors.Is/As on the wrapped cause instead of string matching."],"tags":["network","http-client","lipsync","api-call"],"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-14T00:17:10.932Z"}