sipeed/picoclaw · error

failed to unmarshal response: %w

Error message

failed to unmarshal response: %w

What it means

Thrown when json.Unmarshal(body, &result) fails on a 200 response in ElevenLabsTranscriber.Transcribe. TranscriptionResponse has fields text/language/duration; unknown fields are ignored, so failure means the body is not JSON at all or a field has the wrong type (e.g. language as a number). A 200 with HTML text (captive portal, proxy error page) is the classic trigger.

Source

Thrown at pkg/audio/asr/elevenlabs_transcriber.go:136

	}

	if resp.StatusCode != http.StatusOK {
		logger.ErrorCF("voice", "ElevenLabs API error", map[string]any{
			"status_code": resp.StatusCode,
			"response":    string(body),
		})
		return nil, fmt.Errorf("ElevenLabs API error (status %d): %s", resp.StatusCode, string(body))
	}

	logger.DebugCF("voice", "Received response from ElevenLabs API", map[string]any{
		"status_code":         resp.StatusCode,
		"response_size_bytes": len(body),
	})

	var result TranscriptionResponse
	if err := json.Unmarshal(body, &result); err != nil {
		logger.ErrorCF("voice", "Failed to unmarshal response", map[string]any{"error": err})
		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
	}

	logger.InfoCF("voice", "ElevenLabs transcription completed successfully", map[string]any{
		"text_length":           len(result.Text),
		"language":              result.Language,
		"transcription_preview": utils.Truncate(result.Text, 50),
	})

	return &result, nil
}

func (t *ElevenLabsTranscriber) Name() string {
	return "elevenlabs"
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Log the first ~200 bytes of the body (it is already available in the error path) to see what actually came back.
  2. If a proxy is in play, bypass it or fix its error pages to preserve status codes.
  3. If the endpoint is a custom gateway, point apiBase at the real https://api.elevenlabs.io or adapt the response type.
  4. Decode with json.Decoder + DisallowUnknownFields off, but pre-check content-type to fail fast on non-JSON.

Example fix

// before
var result TranscriptionResponse
if err := json.Unmarshal(body, &result); err != nil {
    return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}

// after: fail fast with context on non-JSON bodies
if !strings.Contains(resp.Header.Get("Content-Type"), "json") {
    return nil, fmt.Errorf("unexpected non-JSON response (content-type %s): %s",
        resp.Header.Get("Content-Type"), utils.Truncate(string(body), 200))
}
var result TranscriptionResponse
if err := json.Unmarshal(body, &result); err != nil {
    return nil, fmt.Errorf("failed to unmarshal response (body prefix %s): %w", utils.Truncate(string(body), 200), err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Cheap probe when using a custom apiBase: assert the endpoint speaks Scribe JSON.
// (Run once against /v1/speech-to-text with a 1-byte wav; expect JSON content-type.)
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "json") {
    return fmt.Errorf("endpoint returned non-JSON content-type %s", ct)
}

Type guard

func isJSONBody(b []byte) bool {
    return json.Valid(b)
}

Try / catch

if _, err := el.Transcribe(ctx, path); err != nil {
    var syntaxErr *json.SyntaxError
    if errors.As(err, &syntaxErr) {
        // 200 + non-JSON body: proxy captive portal or gateway envelope; not retryable as-is
        return errors.New("ElevenLabs endpoint returned 200 with a non-JSON body — check proxy/gateway")
    }
    var typeErr *json.UnmarshalTypeError
    if errors.As(err, &typeErr) {
        return fmt.Errorf("schema drift: field %s has unexpected type", typeErr.Field)
    }
    return err
}

Prevention

When it happens

Trigger: Body is an HTML interstitial from a captive portal or misconfigured proxy that answers 200; a custom apiBase (self-hosted gateway) returns a different JSON shape where 'duration' is a string; truncated body after a connection drop mid-read (ReadAll usually catches this first).

Common situations: Public-WiFi captive portals intercepting HTTPS-less traffic; internal API gateways returning their own envelope {code, data:{text}} instead of the raw Scribe shape; schema drift after an ElevenLabs API version change.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/10b4e5dd491bf0b0. Report an issue: GitHub.