Tencent/WeKnora · error

failed to decode API response

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.

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.

Appendix: 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)