{"record":{"id":"58522d49499c1f8b","repo":"Billionmail/BillionMail","slug":"decode-lipsync-response-w","errorCode":null,"errorMessage":"decode lipsync response: %w","messagePattern":"decode lipsync response: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/lipsync.go","lineNumber":130,"sourceCode":"\tif err != nil {\n\t\treturn \"\", err\n\t}\n\thttpReq = httpReq.WithContext(ctx)\n\n\tresp, err := cfg.doHTTP(httpReq)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"lipsync API call: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn \"\", fmt.Errorf(\"lipsync 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 \"\", 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","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/lipsync.go#L112-L148","documentation":"SubmitLipSync wraps json.Decoder errors when the 2xx response body cannot be decoded into LipSyncResponse. The provider returned a success status but the body was not the expected JSON shape — an HTML error page behind a proxy, an empty body, truncated output, or a schema change to the response fields.","triggerScenarios":"Provider returns 200/201 with an empty body, non-JSON content (HTML login/error page from a proxy or gateway), malformed/truncated JSON, or renamed the id field in a new API version so decoding succeeds structurally but fields shift (this error fires only on syntactically invalid JSON).","commonSituations":"Corporate proxy or CDN intercepting responses; provider outage where a gateway returns 200 with an HTML maintenance page; response body cut off by an intermediary; hitting the wrong URL (e.g. UI endpoint instead of API endpoint) that returns HTML.","solutions":["Capture and inspect the raw response body and Content-Type header before decoding; reject non-JSON content types early","Verify the status-URL/submit-URL points at the API endpoint, not a gateway or dashboard route","Check the provider changelog for response schema changes and update LipSyncResponse field tags","Retry the request — truncated bodies from transient gateway issues often succeed on retry","On failure, log resp.Header.Get(\"Content-Type\") and a body prefix for diagnosis"],"exampleFix":"// before\nvar result LipSyncResponse\nif err := json.NewDecoder(resp.Body).Decode(&result); err != nil { ... }\n// after\nct := resp.Header.Get(\"Content-Type\")\nif !strings.Contains(ct, \"application/json\") {\n    raw, _ := io.ReadAll(resp.Body)\n    return \"\", fmt.Errorf(\"unexpected content-type %q: %.200s\", ct, raw)\n}\nvar result LipSyncResponse\nif err := json.NewDecoder(resp.Body).Decode(&result); err != nil { ... }","handlingStrategy":"type-guard","validationCode":"ct := resp.Header.Get(\"Content-Type\")\nif !strings.Contains(ct, \"application/json\") {\n    raw, _ := io.ReadAll(resp.Body)\n    return fmt.Errorf(\"expected JSON, got %q: %.200s\", ct, raw)\n}","typeGuard":"func looksLikeJSON(ct string, body []byte) bool {\n    if !strings.Contains(ct, \"application/json\") { return false }\n    var probe map[string]any\n    return json.Unmarshal(body, &probe) == nil\n}","tryCatchPattern":"id, err := SubmitLipSync(ctx, cfg, ...)\nif err != nil {\n    if strings.Contains(err.Error(), \"decode lipsync response\") {\n        // log Content-Type and a raw-body prefix; retry once, then surface to ops\n    }\n    return err\n}","preventionTips":["Always check Content-Type before JSON-decoding third-party responses","Pin/monitor the provider API version and subscribe to changelogs","Keep json.RawMessage of unexpected bodies for postmortems","Add a contract test decoding a recorded real response fixture into LipSyncResponse"],"tags":["json","decoding","lipsync","third-party"],"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-12T22:17:10.623Z"}