{"record":{"id":"372232bd2a10f2ba","repo":"Billionmail/BillionMail","slug":"decode-lipsync-status-w","errorCode":null,"errorMessage":"decode lipsync status: %w","messagePattern":"decode lipsync status: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/lipsync.go","lineNumber":156,"sourceCode":"\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)\n\t}\n\n\tresp, err := cfg.doHTTP(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"download lipsync video: %w\", err)","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/lipsync.go#L138-L174","documentation":"After a 200 response, CheckLipSyncStatus decodes the body into LipSyncResponse (ID, Status, VideoURL, Error). This error wraps any json.Decode failure — malformed/truncated JSON, an HTML error page served with status 200, or a connection cut mid-body. It indicates the response was not the expected Sync Labs JSON payload despite the success status code.","triggerScenarios":"A BaseURL override (test mock) returning empty or HTML bodies with 200; a proxy/captive portal intercepting api.synclabs.so; server returning partial JSON due to premature connection close; response Content-Type changed by an intermediary.","commonSituations":"Integration tests where the stub returns '{}' or plain text; corporate proxy rewriting responses; transient network truncation during long polls; Sync Labs API version change altering the payload shape (though Decode tolerates missing fields).","solutions":["Log the raw response body (status 200 path) to see what was actually returned before decoding.","If BaseURL points at a test mock, fix the mock to return valid LipSyncResponse JSON.","Retry transient decode failures — truncation is often network-related and succeeds on the next poll.","Check for proxies/interceptors between the service and the API that could rewrite the body."],"exampleFix":"// before: decode error loses the body context\nvar result video_gen.LipSyncResponse\nif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n    return nil, err\n}\n// after: capture body bytes for diagnostics, then decode\nbody, _ := io.ReadAll(resp.Body)\nvar result video_gen.LipSyncResponse\nif err := json.Unmarshal(body, &result); err != nil {\n    return nil, fmt.Errorf(\"decode lipsync status: %w (body: %q)\", err, string(body))\n}","handlingStrategy":"retry","validationCode":"// inspect response before trusting decode (mocks/proxies)\n// ensure test BaseURL stubs return application/json bodies like {\"id\":\"...\",\"status\":\"pending\"}","typeGuard":"func looksLikeJSON(body []byte) bool {\n    t := bytes.TrimSpace(body)\n    return len(t) > 0 && (t[0] == '{' || t[0] == '[')\n}\nfunc validLipSyncStatus(r *video_gen.LipSyncResponse) bool {\n    return r != nil && r.Status != \"\"\n}","tryCatchPattern":"status, err := video_gen.CheckLipSyncStatus(ctx, cfg, jobID)\nif err != nil {\n    if strings.HasPrefix(err.Error(), \"decode lipsync status\") {\n        // transient/truncated or non-JSON 200: retry after short delay,\n        // and dump the raw body for diagnosis on repeated failure\n    }\n    return err\n}\nif !validLipSyncStatus(status) { return errors.New(\"empty lipsync status payload\") }","preventionTips":["Make test mocks return structurally valid LipSyncResponse JSON.","Bypass or configure proxies for api.synclabs.so in production networks.","Retry decode failures a limited number of times before surfacing.","Log raw bodies on decode errors to distinguish truncation from HTML pages."],"tags":["json","decoding","lipsync","http-response"],"backgroundTag":"json-decode-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"}