Billionmail/BillionMail · error

decode lipsync status

Error message

decode lipsync status: %w

What it means

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.

Solutions

  1. Log the raw response body (status 200 path) to see what was actually returned before decoding.
  2. If BaseURL points at a test mock, fix the mock to return valid LipSyncResponse JSON.
  3. Retry transient decode failures — truncation is often network-related and succeeds on the next poll.
  4. Check for proxies/interceptors between the service and the API that could rewrite the body.

Example fix

// before: decode error loses the body context
var result video_gen.LipSyncResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
    return nil, err
}
// after: capture body bytes for diagnostics, then decode
body, _ := io.ReadAll(resp.Body)
var result video_gen.LipSyncResponse
if err := json.Unmarshal(body, &result); err != nil {
    return nil, fmt.Errorf("decode lipsync status: %w (body: %q)", err, string(body))
}
Defensive patterns

Strategy: retry

Validate before calling

// inspect response before trusting decode (mocks/proxies)
// ensure test BaseURL stubs return application/json bodies like {"id":"...","status":"pending"}

Type guard

func looksLikeJSON(body []byte) bool {
    t := bytes.TrimSpace(body)
    return len(t) > 0 && (t[0] == '{' || t[0] == '[')
}
func validLipSyncStatus(r *video_gen.LipSyncResponse) bool {
    return r != nil && r.Status != ""
}

Try / catch

status, err := video_gen.CheckLipSyncStatus(ctx, cfg, jobID)
if err != nil {
    if strings.HasPrefix(err.Error(), "decode lipsync status") {
        // transient/truncated or non-JSON 200: retry after short delay,
        // and dump the raw body for diagnosis on repeated failure
    }
    return err
}
if !validLipSyncStatus(status) { return errors.New("empty lipsync status payload") }

Prevention

When it happens

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

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

Related errors


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

Appendix: source

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

	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()

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

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

// DownloadLipSyncVideo downloads the completed lip sync video to the output directory.
func DownloadLipSyncVideo(ctx context.Context, cfg LipSyncConfig, videoURL, filename string) (string, error) {
	if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
		return "", fmt.Errorf("create output dir: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "GET", videoURL, nil)
	if err != nil {
		return "", fmt.Errorf("create download request: %w", err)
	}

	resp, err := cfg.doHTTP(req)
	if err != nil {
		return "", fmt.Errorf("download lipsync video: %w", err)

View on GitHub (pinned to fc36c76c05)