{"record":{"id":"10b4e5dd491bf0b0","repo":"sipeed/picoclaw","slug":"failed-to-unmarshal-response-w","errorCode":null,"errorMessage":"failed to unmarshal response: %w","messagePattern":"failed to unmarshal response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/audio/asr/elevenlabs_transcriber.go","lineNumber":136,"sourceCode":"\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tlogger.ErrorCF(\"voice\", \"ElevenLabs API error\", map[string]any{\n\t\t\t\"status_code\": resp.StatusCode,\n\t\t\t\"response\":    string(body),\n\t\t})\n\t\treturn nil, fmt.Errorf(\"ElevenLabs API error (status %d): %s\", resp.StatusCode, string(body))\n\t}\n\n\tlogger.DebugCF(\"voice\", \"Received response from ElevenLabs API\", map[string]any{\n\t\t\"status_code\":         resp.StatusCode,\n\t\t\"response_size_bytes\": len(body),\n\t})\n\n\tvar result TranscriptionResponse\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\tlogger.ErrorCF(\"voice\", \"Failed to unmarshal response\", map[string]any{\"error\": err})\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tlogger.InfoCF(\"voice\", \"ElevenLabs transcription completed successfully\", map[string]any{\n\t\t\"text_length\":           len(result.Text),\n\t\t\"language\":              result.Language,\n\t\t\"transcription_preview\": utils.Truncate(result.Text, 50),\n\t})\n\n\treturn &result, nil\n}\n\nfunc (t *ElevenLabsTranscriber) Name() string {\n\treturn \"elevenlabs\"\n}\n","sourceCodeStart":118,"sourceCodeEnd":151,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/audio/asr/elevenlabs_transcriber.go#L118-L151","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Log the first ~200 bytes of the body (it is already available in the error path) to see what actually came back.","If a proxy is in play, bypass it or fix its error pages to preserve status codes.","If the endpoint is a custom gateway, point apiBase at the real https://api.elevenlabs.io or adapt the response type.","Decode with json.Decoder + DisallowUnknownFields off, but pre-check content-type to fail fast on non-JSON."],"exampleFix":"// before\nvar result TranscriptionResponse\nif err := json.Unmarshal(body, &result); err != nil {\n    return nil, fmt.Errorf(\"failed to unmarshal response: %w\", err)\n}\n\n// after: fail fast with context on non-JSON bodies\nif !strings.Contains(resp.Header.Get(\"Content-Type\"), \"json\") {\n    return nil, fmt.Errorf(\"unexpected non-JSON response (content-type %s): %s\",\n        resp.Header.Get(\"Content-Type\"), utils.Truncate(string(body), 200))\n}\nvar result TranscriptionResponse\nif err := json.Unmarshal(body, &result); err != nil {\n    return nil, fmt.Errorf(\"failed to unmarshal response (body prefix %s): %w\", utils.Truncate(string(body), 200), err)\n}","handlingStrategy":"try-catch","validationCode":"// Cheap probe when using a custom apiBase: assert the endpoint speaks Scribe JSON.\n// (Run once against /v1/speech-to-text with a 1-byte wav; expect JSON content-type.)\nif ct := resp.Header.Get(\"Content-Type\"); !strings.Contains(ct, \"json\") {\n    return fmt.Errorf(\"endpoint returned non-JSON content-type %s\", ct)\n}","typeGuard":"func isJSONBody(b []byte) bool {\n    return json.Valid(b)\n}","tryCatchPattern":"if _, err := el.Transcribe(ctx, path); err != nil {\n    var syntaxErr *json.SyntaxError\n    if errors.As(err, &syntaxErr) {\n        // 200 + non-JSON body: proxy captive portal or gateway envelope; not retryable as-is\n        return errors.New(\"ElevenLabs endpoint returned 200 with a non-JSON body — check proxy/gateway\")\n    }\n    var typeErr *json.UnmarshalTypeError\n    if errors.As(err, &typeErr) {\n        return fmt.Errorf(\"schema drift: field %s has unexpected type\", typeErr.Field)\n    }\n    return err\n}","preventionTips":["Point apiBase at the real API or a faithful gateway, not an enveloping proxy.","Check Content-Type before parsing in your own HTTP code.","Pin API versions; re-run contract tests after gateway upgrades."],"tags":["json","parsing","elevenlabs","proxy","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}