Billionmail/BillionMail · error

decode lipsync response: %w

Error message

decode lipsync response: %w

What it means

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.

Source

Thrown at core/internal/service/video_gen/lipsync.go:130

	if err != nil {
		return "", err
	}
	httpReq = httpReq.WithContext(ctx)

	resp, err := cfg.doHTTP(httpReq)
	if err != nil {
		return "", fmt.Errorf("lipsync API call: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		body, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("lipsync API error %d: %s", resp.StatusCode, string(body))
	}

	var result LipSyncResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", fmt.Errorf("decode lipsync response: %w", err)
	}
	return result.ID, nil
}

// CheckLipSyncStatus checks the status of a lip sync job.
func CheckLipSyncStatus(ctx context.Context, cfg LipSyncConfig, jobID string) (*LipSyncResponse, error) {
	httpReq, err := BuildLipSyncStatusRequest(cfg, jobID)
	if err != nil {
		return nil, err
	}
	httpReq = httpReq.WithContext(ctx)

	resp, err := cfg.doHTTP(httpReq)
	if err != nil {
		return nil, fmt.Errorf("lipsync status API call: %w", err)
	}
	defer resp.Body.Close()

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Capture and inspect the raw response body and Content-Type header before decoding; reject non-JSON content types early
  2. Verify the status-URL/submit-URL points at the API endpoint, not a gateway or dashboard route
  3. Check the provider changelog for response schema changes and update LipSyncResponse field tags
  4. Retry the request — truncated bodies from transient gateway issues often succeed on retry
  5. On failure, log resp.Header.Get("Content-Type") and a body prefix for diagnosis

Example fix

// before
var result LipSyncResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { ... }
// after
ct := resp.Header.Get("Content-Type")
if !strings.Contains(ct, "application/json") {
    raw, _ := io.ReadAll(resp.Body)
    return "", fmt.Errorf("unexpected content-type %q: %.200s", ct, raw)
}
var result LipSyncResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { ... }
Defensive patterns

Strategy: type-guard

Validate before calling

ct := resp.Header.Get("Content-Type")
if !strings.Contains(ct, "application/json") {
    raw, _ := io.ReadAll(resp.Body)
    return fmt.Errorf("expected JSON, got %q: %.200s", ct, raw)
}

Type guard

func looksLikeJSON(ct string, body []byte) bool {
    if !strings.Contains(ct, "application/json") { return false }
    var probe map[string]any
    return json.Unmarshal(body, &probe) == nil
}

Try / catch

id, err := SubmitLipSync(ctx, cfg, ...)
if err != nil {
    if strings.Contains(err.Error(), "decode lipsync response") {
        // log Content-Type and a raw-body prefix; retry once, then surface to ops
    }
    return err
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/58522d49499c1f8b. Report an issue: GitHub.