fish2018/pansou · error

[ ] 读取 API 响应失败

Error message

[%s] 读取 API 响应失败: %w

What it means

searchImpl reads the entire API response body with io.ReadAll; a failure here (premature connection close, read timeout, truncated chunked encoding) triggers the searchWeb fallback with this wrapped error. The response arrived but its body could not be fully consumed.

Solutions

  1. Inspect the wrapped io error (unexpected EOF vs context deadline)
  2. Increase DefaultTimeout if the deadline fires during body read
  3. Retry the request; add the body read inside doRequestWithRetry for retry coverage
  4. Prefer the searchWeb fallback results when API is flaky

Example fix

// before
body, err := io.ReadAll(resp.Body)
if err != nil {
    return p.searchWeb(client, keyword, fmt.Errorf("[%s] 读取 API 响应失败: %w", p.Name(), err))
}
// after
body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        return p.searchWeb(client, keyword, fmt.Errorf("[%s] 读取 API 响应超时: %w", p.Name(), err))
    }
    return p.searchWeb(client, keyword, fmt.Errorf("[%s] 读取 API 响应失败: %w", p.Name(), err))
}
Defensive patterns

Strategy: retry

Validate before calling

// check response completeness signals
// (pre-request there is nothing to validate; ensure client timeouts are set)
client := &http.Client{Timeout: 30 * time.Second}

Try / catch

results, err := plugin.Search(keyword)
if err != nil && strings.Contains(err.Error(), "读取 API 响应失败") {
    if errors.Is(err, context.DeadlineExceeded) {
        time.Sleep(time.Second)
        results, err = plugin.Search(keyword) // one bounded retry
    }
}

Prevention

When it happens

Trigger: io.ReadAll(resp.Body) returns non-nil — connection reset mid-body, context deadline hit while streaming, invalid chunked transfer encoding.

Common situations: Flaky upstream connection, server/proxy cutting large responses short, DefaultTimeout expiring during body read, unstable network or VPN.

Related errors


AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07). Data as JSON: /api/errors/2d1a56900f491b8b. Report an issue: GitHub.

Appendix: source

Thrown at plugin/feikuai/feikuai.go:139

	req.Header.Set("Connection", "keep-alive")
	req.Header.Set("Referer", "https://feikuai.tv/")

	// 发送请求(带重试)
	resp, err := p.doRequestWithRetry(req, client)
	if err != nil {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] 搜索 API 请求失败: %w", p.Name(), err))
	}
	defer resp.Body.Close()

	// 检查状态码
	if resp.StatusCode != 200 {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] 搜索 API 返回状态码: %d", p.Name(), resp.StatusCode))
	}

	// 读取并解析JSON响应
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] 读取 API 响应失败: %w", p.Name(), err))
	}

	var apiResp FeikuaiAPIResponse
	if err := json.Unmarshal(body, &apiResp); err != nil {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] API JSON 解析失败: %w", p.Name(), err))
	}

	// 检查API响应状态
	if apiResp.Code != 0 {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] API 返回错误: %s (code: %d)", p.Name(), apiResp.Msg, apiResp.Code))
	}

	// 解析搜索结果
	var results []model.SearchResult
	for _, item := range apiResp.Items {
		// 每个item可能包含多个种子
		for _, torrent := range item.Torrents {
			result := p.parseTorrent(keyword, item, torrent)

View on GitHub (pinned to beaa561337)