Tencent/WeKnora · error
failed to decode API response: %w
Error message
failed to decode API response: %w
What it means
Returned by searchAPI when the JSON response body from the DuckDuckGo instant-answer API cannot be decoded into the anonymous response struct. Means DuckDuckGo returned 200 with malformed JSON, an HTML error page, or an empty/truncated body — the search fails for this call even though HTTP reported success.
Source
Thrown at internal/infrastructure/web_search/duckduckgo.go:178
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("duckduckgo API returned status %d: %s", resp.StatusCode, string(body))
}
var apiResponse struct {
AbstractText string `json:"AbstractText"`
AbstractURL string `json:"AbstractURL"`
Heading string `json:"Heading"`
RelatedTopics []struct {
FirstURL string `json:"FirstURL"`
Text string `json:"Text"`
} `json:"RelatedTopics"`
Results []struct {
FirstURL string `json:"FirstURL"`
Text string `json:"Text"`
} `json:"Results"`
}
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
return nil, fmt.Errorf("failed to decode API response: %w", err)
}
results := make([]*types.WebSearchResult, 0, maxResults)
if apiResponse.AbstractText != "" && apiResponse.AbstractURL != "" {
results = append(results, &types.WebSearchResult{
Title: apiResponse.Heading,
URL: apiResponse.AbstractURL,
Snippet: apiResponse.AbstractText,
Source: "duckduckgo",
})
}
for _, topic := range apiResponse.RelatedTopics {
if len(results) >= maxResults {
break
}
if topic.Text != "" && topic.FirstURL != "" {
results = append(results, &types.WebSearchResult{
Title: extractTitle(topic.Text),View on GitHub (pinned to 988cbb0330)
Solutions
- Log the first bytes of resp.Body on decode failure to see what was actually returned
- Check for HTML/bot-block pages and switch IP/User-Agent
- Verify the struct tags still match the current DuckDuckGo API schema
- Use json.Unmarshal on a bounded-read body to get a clearer error offset
Example fix
// before
if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
return nil, fmt.Errorf("failed to decode API response: %w", err)
}
// after
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err := json.Unmarshal(body, &apiResponse); err != nil {
return nil, fmt.Errorf("failed to decode API response (body head: %.200s): %w", string(body), err)
} Defensive patterns
Strategy: type-guard
Validate before calling
ct := resp.Header.Get("Content-Type")
if !strings.Contains(ct, "application/json") {
return fmt.Errorf("expected JSON, got %s", ct)
} Type guard
func isJSONBody(head []byte) bool {
h := bytes.TrimLeft(head, " \t\r\n")
return len(h) > 0 && (h[0] == '{' || h[0] == '[')
} Try / catch
results, err := provider.Search(ctx, query, 10, false)
if err != nil && strings.Contains(err.Error(), "failed to decode API response") {
log.Printf("duckduckgo returned non-JSON (bot block?): %v", err)
return fallbackProvider.Search(ctx, query, 10, false)
} Prevention
- Inspect Content-Type before decoding
- Log a bounded snippet of the body on decode failure
- Keep struct tags in sync with the documented API schema
- Detect HTML/captcha pages and rotate IP or User-Agent
When it happens
Trigger: Calling Search -> searchAPI when the response is HTML (a block/captcha page), truncated, an error JSON with a different schema, or content-encoding mishandled by a proxy.
Common situations: DuckDuckGo serving a bot-detection HTML page instead of JSON, proxies rewriting responses, API schema changes, reading a 200 body that is empty or cut off.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- decode extract_result array: %w
- decode extract_result object: %w
- unmarshal page: %w
- decode qqbot response: %w
- decode poll response: %w
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/c7a8c97d14e9277e.
Report an issue: GitHub.