fish2018/pansou · error
[ ] 读取响应失败
Error message
[%s] 读取响应失败: %w
What it means
Reading the ASH response body with io.ReadAll on a 2MB LimitReader failed. The plugin wraps the I/O error as '读取响应失败'. This means the connection broke mid-body (EOF, reset, unexpected EOF) rather than the content being too large — the LimitReader caps size, not correctness.
Solutions
- Retry the request (the existing retry loop does not cover body reading); a single re-issue usually succeeds.
- Unwrap the error to distinguish context deadline exceeded (raise the 15s timeout) from connection reset (network/site issue).
- Read incrementally with buffered reads so partial data and the exact failure point can be logged.
- Check for proxies/middleboxes truncating responses and test from a different network.
Example fix
// before
body, err := io.ReadAll(limitReader)
if err != nil {
return nil, fmt.Errorf("[%s] 读取响应失败: %w", p.Name(), err)
}
// after
body, err := io.ReadAll(limitReader)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("[%s] 读取响应超时: %w", p.Name(), err)
}
return nil, fmt.Errorf("[%s] 读取响应失败: %w", p.Name(), err)
} Defensive patterns
Strategy: retry
Validate before calling
// re-issue the request if body read fails once
if err != nil && errors.Is(err, io.ErrUnexpectedEOF) { retry once } Try / catch
body, err := io.ReadAll(limitReader)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) { /* raise timeout, retry */ }
else { /* retry request; log truncated read */ }
} Prevention
- Cover body reads inside the retry loop, not just the request
- Use timeouts larger than worst-case streaming time
- Avoid unreliable proxies for large responses
- Log bytes-read count on failure for diagnosis
When it happens
Trigger: io.ReadAll(limitReader) on resp.Body returns a non-nil error: connection reset by peer, unexpected EOF, context deadline exceeded while streaming — ash.go:107.
Common situations: Server closes the connection early under load or anti-bot filtering; flaky mobile/proxied network; TLS interception cutting the stream; very slow site exceeding the 15s context timeout mid-read.
Understand the failure class
Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.
Related errors
AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07).
Data as JSON: /api/errors/522f1982dab881b1.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/ash/ash.go:107
// 发送请求(优化重试)
resp, err := p.doRequestWithRetry(req, client)
if err != nil {
return nil, fmt.Errorf("[%s] 搜索请求失败: %w", p.Name(), err)
}
defer resp.Body.Close()
// 检查状态码
if resp.StatusCode != 200 {
return nil, fmt.Errorf("[%s] 请求返回状态码: %d", p.Name(), resp.StatusCode)
}
// 读取响应(使用有限制的读取,避免读取过大内容)
// ASH页面通常不会太大,限制在2MB以内
limitReader := io.LimitReader(resp.Body, 2*1024*1024)
body, err := io.ReadAll(limitReader)
if err != nil {
return nil, fmt.Errorf("[%s] 读取响应失败: %w", p.Name(), err)
}
// 从HTML中提取JSON数据(直接传递字节,避免字符串转换)
results, err := p.extractResultsFromBytes(body)
if err != nil {
return nil, fmt.Errorf("[%s] 提取搜索结果失败: %w", p.Name(), err)
}
// 关键词过滤
filtered := plugin.FilterResultsByKeyword(results, keyword)
return filtered, nil
}
// extractResultsFromBytes 从字节数组中提取搜索结果(优化版本,避免字符串转换)
func (p *AshPlugin) extractResultsFromBytes(data []byte) ([]model.SearchResult, error) {
// 直接在字节数组中查找JSON数据(避免转换为字符串)
html := string(data) // 只转换一次View on GitHub (pinned to beaa561337)