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
- Log the raw response body on decode failure to see what was actually returned.
- Confirm Cartesia-Version header matches a version whose clone response has {id,name}.
- Check for proxies/gateways rewriting responses; test from a clean network.
- Use json.NewDecoder with a fresh read and check for EOF/empty body before decoding.
- 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
- Pin and periodically review the Cartesia-Version header against the docs
- Log raw bodies on decode failures to catch schema drift early
- Test against the real API shape (or recorded fixtures) in CI
- Watch for proxies/AV injecting HTML into API responses
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
- decode lipsync response: %w
- decode lipsync status: %w
- voice clone: %w
- marshal clone request: %w
- marshal TTS request: %w
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/36f1f7459a965719.
Report an issue: GitHub.