Tencent/WeKnora · error

failed to unmarshal response: %w

Error message

failed to unmarshal response: %w

What it means

Returned by KeenableProvider.Search when json.Unmarshal cannot decode the API response body into keenableSearchResponse. The body was received and read successfully, but it is not the JSON shape the provider expects — often HTML from an error/login page, truncated JSON, or a schema change at the Keenable API. The json error is wrapped for inspection.

Source

Thrown at internal/infrastructure/web_search/keenable.go:117

	if err != nil {
		return nil, fmt.Errorf("failed to execute request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		respBody, _ := io.ReadAll(resp.Body)
		logger.Warnf(ctx, "[WebSearch][Keenable] API returned status %d: %s", resp.StatusCode, string(respBody))
		return nil, fmt.Errorf("keenable API returned status %d: %s", resp.StatusCode, string(respBody))
	}

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	var respData keenableSearchResponse
	if err := json.Unmarshal(respBody, &respData); err != nil {
		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
	}

	results := make([]*types.WebSearchResult, 0, len(respData.Results))
	for _, item := range respData.Results {
		if maxResults > 0 && len(results) >= maxResults {
			break
		}
		// Keenable returns both a short "description" and a longer "snippet"
		// excerpt. Prefer the short description as the summary snippet and keep
		// the longer excerpt as Content (used by RAG compression). Fall back to
		// whichever is present so we never drop the only available text.
		snippet := item.Description
		if snippet == "" {
			snippet = item.Snippet
		}
		result := &types.WebSearchResult{
			Title:   item.Title,
			URL:     item.URL,

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Log/print the raw respBody on failure to see whether it is HTML, empty, or truncated JSON.
  2. If the body is HTML, look for an intercepting proxy, captive portal, or wrong baseURL in the provider config.
  3. Compare the actual JSON against the keenableSearchResponse struct fields/types; update the struct if the API schema changed.
  4. Pin or update the provider version to match the current Keenable API contract.

Example fix

// before: opaque unmarshal failure
if err := json.Unmarshal(respBody, &respData); err != nil {
    return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}

// after: include a body snippet for diagnosis
if err := json.Unmarshal(respBody, &respData); err != nil {
    return nil, fmt.Errorf("failed to unmarshal response: %w (body: %.200s)", err, string(respBody))
}
Defensive patterns

Strategy: try-catch

Type guard

func isLikelyJSON(b []byte) bool {
    t := bytes.TrimSpace(b)
    return len(t) > 0 && (t[0] == '{' || t[0] == '[')
}

// call before unmarshalling:
if !isLikelyJSON(respBody) {
    return nil, fmt.Errorf("non-JSON response body: %.200s", string(respBody))
}

Try / catch

if err := json.Unmarshal(respBody, &respData); err != nil {
    log.Printf("keenable unmarshal failed: %v, body=%.500s", err, string(respBody))
    return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}

Prevention

When it happens

Trigger: Search (internal/infrastructure/web_search/keenable.go:117) read a 200 body but json.Unmarshal(respBody, &respData) fails — non-JSON HTML body, malformed/truncated JSON, or fields whose types no longer match keenableSearchResponse.

Common situations: Proxy/captive portal returning HTML with status 200; Keenable API contract change after a version bump; CDN error interstitial; body cut off mid-stream by a short client timeout.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


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