Billionmail/BillionMail · error

decode clone response: %w

Error message

decode clone response: %w

What it means

After a successful (HTTP 200) call to Cartesia /voices/clone, CloneVoice decodes the response body into VoiceCloneResponse{ID,Name}. If the body is not valid JSON or does not match the expected shape, json.Decoder returns an error wrapped as 'decode clone response'. This happens when the endpoint returns 200 with an unexpected payload (proxy interference, API shape change, HTML error page).

Source

Thrown at core/internal/service/video_gen/voice.go:161

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

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

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

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

// TextToSpeech generates audio from text using a Cartesia voice.
// Returns the path to the output WAV file.
func TextToSpeech(ctx context.Context, cfg VoiceConfig, voiceID, transcript, filename string) (string, error) {
	if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
		return "", fmt.Errorf("create output dir: %w", err)
	}

	req := TTSRequest{
		VoiceID:      voiceID,
		Transcript:   transcript,
		ModelID:      "sonic-2",
		OutputFormat: DefaultTTSOutputFormat(),
		Language:     "en",
	}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Log the raw response body on decode failure to see what was actually returned.
  2. Confirm Cartesia-Version header matches a version whose clone response has {id,name}.
  3. Check for proxies/gateways rewriting responses; test from a clean network.
  4. Use json.NewDecoder with a fresh read and check for EOF/empty body before decoding.
  5. Pin/upgrade the API version header to the current documented one.

Example fix

// before
var result VoiceCloneResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
    return nil, fmt.Errorf("decode clone response: %w", err)
}
// after
raw, _ := io.ReadAll(resp.Body)
var result VoiceCloneResponse
if err := json.Unmarshal(raw, &result); err != nil {
    return nil, fmt.Errorf("decode clone response %q: %w", string(raw), err)
}
if result.ID == "" {
    return nil, fmt.Errorf("clone response missing id: %s", string(raw))
}
Defensive patterns

Strategy: type-guard

Type guard

func isValidCloneResponse(r *video_gen.VoiceCloneResponse) bool {
    return r != nil && r.ID != ""
}

// usage
result, err := video_gen.CloneVoice(ctx, cfg, name, audioURL)
if err != nil {
    return fmt.Errorf("clone voice: %w", err)
}
if !isValidCloneResponse(result) {
    return errors.New("clone response missing voice id")
}

Try / catch

result, err := video_gen.CloneVoice(ctx, cfg, name, audioURL)
if err != nil {
    if strings.Contains(err.Error(), "decode clone response") {
        log.Printf("unexpected cartesia response, check API version/proxy: %v", err)
        return err
    }
    return err
}

Prevention

When it happens

Trigger: Cartesia returns 200 with an empty body, a changed JSON schema (field renames), a gateway/CDN HTML interstitial, or a truncated response due to network interruption mid-read.

Common situations: Corporate proxy or antivirus injecting HTML pages, Cartesia API version drift changing the response format, nil/empty body on 200, truncation from connection reset mid-body.

Related errors


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