{"record":{"id":"a85aa0d5db74f34f","repo":"sipeed/picoclaw","slug":"parse-response-w","errorCode":null,"errorMessage":"parse response: %w","messagePattern":"parse response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/membench/llm_client.go","lineNumber":180,"sourceCode":"\n\t\tif resp.StatusCode == 429 || resp.StatusCode >= 500 {\n\t\t\tlastErr = fmt.Errorf(\"API error %d: %s\", resp.StatusCode, string(respBody))\n\t\t\tcontinue // rate limit or server error → retry\n\t\t}\n\t\tif resp.StatusCode != 200 {\n\t\t\treturn \"\", fmt.Errorf(\"API error %d: %s\", resp.StatusCode, string(respBody))\n\t\t}\n\n\t\tlastErr = nil\n\t\tbreak\n\t}\n\tif lastErr != nil {\n\t\treturn \"\", fmt.Errorf(\"after %d retries: %w\", c.MaxRetries, lastErr)\n\t}\n\n\tvar chatResp chatResponse\n\tif err := json.Unmarshal(respBody, &chatResp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"parse response: %w\", err)\n\t}\n\tif len(chatResp.Choices) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no choices in response\")\n\t}\n\tcontent := strings.TrimSpace(chatResp.Choices[0].Message.Content)\n\t// Strip any residual <think>...</think> blocks\n\tif idx := strings.Index(content, \"</think>\"); idx >= 0 {\n\t\tcontent = strings.TrimSpace(content[idx+len(\"</think>\"):])\n\t}\n\t// Fallback: GLM/DeepSeek put thinking output in reasoning_content when thinking is enabled\n\tif content == \"\" && chatResp.Choices[0].Message.ReasoningContent != \"\" {\n\t\tcontent = strings.TrimSpace(chatResp.Choices[0].Message.ReasoningContent)\n\t}\n\tif content == \"\" {\n\t\treturn \"\", fmt.Errorf(\"empty LLM response\")\n\t}\n\treturn content, nil\n}","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/cmd/membench/llm_client.go#L162-L198","documentation":"json.Unmarshal of the HTTP response body into chatResponse failed (llm_client.go:180). The server returned 200 but the body is not valid JSON for this struct — an HTML error page from a reverse proxy, an empty or truncated body, a differently-shaped JSON (missing fields is fine, wrong types are not), or a BOM/whitespace prefix. The error text names byte offset, which localizes the mismatch.","triggerScenarios":"BaseURL points at a web UI or gateway that answers 200 with HTML; proxy (nginx, Cloudflare) intercepting with a status page; response field types differing (e.g. usage as string vs object); chunked body cut off mid-transfer; Ollama version returning a non-OpenAI schema at the wrong path.","commonSituations":"Using an OpenAI-compatible client against a non-compatible endpoint; BaseURL missing /v1 so the root handler responds 200 HTML; older/newer provider schema drift; corporate proxy rewriting responses.","solutions":["curl -sS $BASE/chat/completions -H 'Content-Type: application/json' -d @req.json and eyeball the actual body","Fix BaseURL to the exact OpenAI-compatible path (commonly http://host:port/v1)","Log the first ~512 bytes of respBody on parse failure to see what arrived","If fields differ, extend chatResponse with json.RawMessage or custom UnmarshalJSON instead of failing"],"exampleFix":"// before\nif err := json.Unmarshal(respBody, &chatResp); err != nil {\n    return \"\", fmt.Errorf(\"parse response: %w\", err)\n}\n\n// after\nif err := json.Unmarshal(respBody, &chatResp); err != nil {\n    snippet := string(respBody)\n    if len(snippet) > 512 { snippet = snippet[:512] }\n    return \"\", fmt.Errorf(\"parse response (status 200, body starts with %q): %w\", snippet, err)\n}","handlingStrategy":"try-catch","validationCode":"ct := resp.Header.Get(\"Content-Type\")\nif ct != \"\" && !strings.Contains(ct, \"json\") {\n    return \"\", fmt.Errorf(\"non-JSON response (%s) from %s — wrong endpoint?\", ct, endpoint)\n}","typeGuard":null,"tryCatchPattern":"var chatResp chatResponse\nif err := json.Unmarshal(respBody, &chatResp); err != nil {\n    head := string(respBody)\n    if len(head) > 256 { head = head[:256] }\n    return \"\", fmt.Errorf(\"parse response (status %d, body %q): %w\", resp.StatusCode, head, err)\n}","preventionTips":["Verify BaseURL includes the OpenAI-compatible path (commonly /v1) — a bare host often returns HTML at 200","Check Content-Type contains application/json before unmarshalling","Log a body snippet on parse failure; the HTML title usually names the proxy at fault","Beware providers whose schemas drift — keep tolerant types (json.RawMessage) for volatile fields"],"tags":["go","json","llm-client","response","proxy"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}