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

  1. Log the first bytes of resp.Body on decode failure to see what was actually returned
  2. Check for HTML/bot-block pages and switch IP/User-Agent
  3. Verify the struct tags still match the current DuckDuckGo API schema
  4. 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

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

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/c7a8c97d14e9277e. Report an issue: GitHub.