Tencent/WeKnora · error

failed to unmarshal Exa response: %w

Error message

failed to unmarshal Exa response: %w

What it means

The Exa provider failed to decode the HTTP response body into exaSearchResponse via json.Unmarshal. This means Exa returned a 2xx response whose body is not the expected JSON shape — commonly HTML (an error/interstitial page), an empty body, or a schema that changed.

Source

Thrown at internal/infrastructure/web_search/exa.go:111

	logger.Infof(ctx, "[WebSearch][Exa] query=%q maxResults=%d url=%s", query, maxResults, p.baseURL)
	resp, err := p.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to execute Exa request: %w", err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxExaResponseBytes))
	if err != nil {
		return nil, fmt.Errorf("failed to read Exa response: %w", err)
	}
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		logger.Warnf(ctx, "[WebSearch][Exa] API returned status %d: %s", resp.StatusCode, string(body))
		return nil, fmt.Errorf("exa API returned status %d: %s", resp.StatusCode, string(body))
	}

	var data exaSearchResponse
	if err := json.Unmarshal(body, &data); err != nil {
		return nil, fmt.Errorf("failed to unmarshal Exa response: %w", err)
	}
	if data.Error != "" {
		return nil, fmt.Errorf("exa API error: %s", data.Error)
	}

	results := make([]*types.WebSearchResult, 0, len(data.Results))
	for _, item := range data.Results {
		if len(results) >= maxResults {
			break
		}
		snippet := strings.TrimSpace(strings.Join(item.Highlights, "\n"))
		content := truncateExaText(strings.TrimSpace(item.Text), maxExaContentRunes)
		if snippet == "" {
			snippet = truncateExaText(content, 500)
		}
		result := &types.WebSearchResult{Title: item.Title, URL: item.URL, Snippet: snippet, Content: content, Source: "exa"}
		if includeDate && item.PublishedDate != "" {
			if publishedAt, err := time.Parse(time.RFC3339, item.PublishedDate); err == nil {

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Log/capture the raw response body to see what was actually returned (often HTML from a proxy).
  2. Verify no proxy or middleware is rewriting the response; bypass the proxy to test.
  3. Pin/check the Exa API version and confirm your provider struct fields match the current response schema.
  4. Add a content-type check before unmarshalling to fail with a clearer message.

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: hit a cheap Exa endpoint and check Content-Type is application/json
resp, _ := http.Get(baseURL)
if !strings.Contains(resp.Header.Get("Content-Type"), "application/json") {
    return errors.New("exa response is not JSON — check proxy")
}

Try / catch

if err != nil && strings.Contains(err.Error(), "failed to unmarshal Exa response") {
    // capture raw body via logs and inspect; fall back to another provider
    return fallbackProvider.Search(ctx, q, n, false)
}

Prevention

When it happens

Trigger: json.Unmarshal on the (already 2xx) Exa response body returns an error — invalid JSON syntax, unexpected types, or empty body despite HTTP 200.

Common situations: A proxy or captive portal returning HTML with status 200; Exa API version drift changing field types; truncation issues; network middleware injecting content.

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/97fe39117a1ed048. Report an issue: GitHub.