fish2018/pansou · error
[ ] 解析搜索页面失败
Error message
[%s] 解析搜索页面失败: %w
What it means
searchImpl wraps a goquery.NewDocumentFromReader failure as "[%s] 解析搜索页面失败". goquery parsing only fails when reading the underlying response body fails, so this means the HTML document could not be fully read/decoded — typically a truncated or reset connection during body transfer.
Solutions
- Retry the search; mid-body resets are usually transient.
- Read the body manually with io.ReadAll first, then parse with goquery.NewDocumentFromReader to separate I/O errors from parse issues and enable logging of partial content.
- Check proxy/VPN interference and ensure the client handles Content-Encoding (gzip) consistently with the Accept-Encoding header sent in setCommonHeaders.
- Increase retry/backoff so doRequestWithRetry absorbs these resets.
Example fix
// before
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, fmt.Errorf("[%s] 解析搜索页面失败: %w", p.Name(), err)
}
// after
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("[%s] 读取搜索页面失败: %w", p.Name(), err)
}
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("[%s] 解析搜索页面失败: %w", p.Name(), err)
} Defensive patterns
Strategy: retry
Try / catch
results, err := plugin.Search(ctx, keyword)
if err != nil && strings.Contains(err.Error(), "解析搜索页面失败") {
// body transfer failed; retry once after a short delay
time.Sleep(time.Second)
results, err = plugin.Search(ctx, keyword)
} Prevention
- Retry automatically once; truncated HTML responses are typically transient.
- Ensure Accept-Encoding matches the encodings your client can decode.
- Read the body fully before parsing so I/O and parse failures are distinguishable.
When it happens
Trigger: goquery.NewDocumentFromReader(resp.Body) returns an error: the connection was closed mid-body (unexpected EOF, connection reset) while streaming the search results HTML.
Common situations: Server/LB cutting large search-result pages, flaky proxy or VPN connections, gzip/deflate body mismatch when the client does not handle the advertised encoding, or the site throttling by dropping connections.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07).
Data as JSON: /api/errors/188a5150e577cfb6.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/daishudj/daishudj.go:147
req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
if err != nil {
return nil, fmt.Errorf("[%s] 创建搜索请求失败: %w", p.Name(), err)
}
setCommonHeaders(req, "https://www.daishuduanju.com/")
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 != http.StatusOK {
return nil, fmt.Errorf("[%s] 搜索返回状态码: %d", p.Name(), resp.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, fmt.Errorf("[%s] 解析搜索页面失败: %w", p.Name(), err)
}
var (
results []model.SearchResult
wg sync.WaitGroup
mu sync.Mutex
sem = make(chan struct{}, maxConcurrency)
)
doc.Find(".item-jx.item-blog").Each(func(_ int, item *goquery.Selection) {
titleSel := item.Find(".subtitle h5 a")
title := strings.TrimSpace(titleSel.Text())
detailURL, ok := titleSel.Attr("href")
if !ok || title == "" || detailURL == "" {
return
}
postID := extractPostID(detailURL)View on GitHub (pinned to beaa561337)