{"record":{"id":"868666af258fb4f2","repo":"sipeed/picoclaw","slug":"failed-to-decode-response-w","errorCode":null,"errorMessage":"failed to decode response: %w","messagePattern":"failed to decode response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/audio/tts/mimo_tts.go","lineNumber":149,"sourceCode":"\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"API error (status %d): %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar payload struct {\n\t\tChoices []struct {\n\t\t\tMessage struct {\n\t\t\t\tAudio struct {\n\t\t\t\t\tData string `json:\"data\"`\n\t\t\t\t} `json:\"audio\"`\n\t\t\t} `json:\"message\"`\n\t\t} `json:\"choices\"`\n\t}\n\n\terr = json.Unmarshal(body, &payload)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode response: %w\", err)\n\t}\n\n\tif len(payload.Choices) == 0 || payload.Choices[0].Message.Audio.Data == \"\" {\n\t\treturn nil, fmt.Errorf(\"invalid TTS response: missing audio data\")\n\t}\n\n\taudioBytes, err := base64.StdEncoding.DecodeString(payload.Choices[0].Message.Audio.Data)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode audio data: %w\", err)\n\t}\n\n\treturn io.NopCloser(bytes.NewReader(audioBytes)), nil\n}\n","sourceCodeStart":131,"sourceCodeEnd":163,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/audio/tts/mimo_tts.go#L131-L163","documentation":"Returned by MimoTTSProvider.Synthesize when a 200-status body does not match the expected JSON shape (choices[].message.audio.data). json.Unmarshal fails when the body is an HTML error page delivered with HTTP 200 (captive portal/gateway), empty, or when the provider changes its response schema.","triggerScenarios":"A transparent proxy or captive portal returns an HTML block page with status 200; the resolved apiBase drifted away from /v1/chat/completions; the provider shipped a schema change.","commonSituations":"Guest/portal Wi-Fi; custom api_base normalization mistakes; provider API version bumps.","solutions":["Log the raw body alongside the unmarshal error to see what actually arrived","Verify the resolved apiBase ends with /v1/chat/completions for api.xiaomimimo.com","Whitelist the API host through proxies/captive portals","Track provider changelogs for schema changes and update the payload struct"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isJSONShapeError(err error) bool {\n    var se *json.SyntaxError\n    var te *json.UnmarshalTypeError\n    return errors.As(err, &se) || errors.As(err, &te)\n}","tryCatchPattern":"if err != nil && isJSONShapeError(err) {\n    // 200 with non-contract JSON: check for captive portals/proxies on the path\n    // and confirm apiBase still resolves to /v1/chat/completions\n}","preventionTips":["Whitelist api.xiaomimimo.com through proxies and captive portals","Log the raw body when decode fails to distinguish HTML gateways from schema drift","Monitor provider changelogs for response schema changes"],"tags":["json","api","tts","mimo","schema"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}