Tencent/WeKnora · error

failed to read response: %w

Error message

failed to read response: %w

What it means

Returned by KeenableProvider.Search when io.ReadAll(resp.Body) fails while reading the successful HTTP response body. This means the connection broke or timed out mid-transfer after the server already sent a 200 status, so the body could not be fully read. The underlying read error is wrapped with %w for errors.Is/errors.As inspection.

Source

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

	if p.apiKey != "" {
		req.Header.Set("X-API-Key", p.apiKey)
	}

	resp, err := p.client.Do(req)
	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 == "" {

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Unwrap with errors.Is(err, context.DeadlineExceeded) / net.Error.Timeout() to distinguish timeouts from resets, and increase the client timeout or context deadline.
  2. Retry the request with exponential backoff — this is typically a transient network fault.
  3. Verify network path: disable or fix proxies/VPNs that may be truncating the response.
  4. Check server-side: if it reproduces for large responses only, reduce maxResults to shrink the body.

Example fix

// before: fail hard on any read error
if err != nil { return nil, err }

// after: retry transient read failures
body, err := readWithRetry(ctx, client, req, 3)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        return nil, fmt.Errorf("keenable read timed out: %w", err)
    }
    return nil, err
}
Defensive patterns

Strategy: retry

Validate before calling

if dl, ok := ctx.Deadline(); !ok || time.Until(dl) < 10*time.Second {
    var cancel context.CancelFunc
    ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
}

Try / catch

body, err := io.ReadAll(resp.Body)
if err != nil {
    var ne net.Error
    if errors.As(err, &ne) && ne.Timeout() || errors.Is(err, context.DeadlineExceeded) {
        return nil, fmt.Errorf("transient read failure, retrying: %w", err)
    }
    return nil, fmt.Errorf("failed to read response: %w", err)
}

Prevention

When it happens

Trigger: Search (internal/infrastructure/web_search/keenable.go:112) got a 200 response but io.ReadAll(resp.Body) returned an error — connection reset by peer, context deadline exceeded mid-body, TLS truncation, or proxy dropping the stream.

Common situations: Flaky network or mobile connections; aggressive upstream load balancer timeouts; ctx cancellation/deadline shorter than Keenable's response time; misbehaving corporate proxy.

Related errors


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