fish2018/pansou · error
[ ] 解析首页失败
Error message
[%s] 解析首页失败: %w
What it means
Returned when goquery.NewDocumentFromReader fails to parse the homepage response body as HTML. This wraps the goquery/html parsing error, which almost always means the body is empty, truncated, or not valid HTML (e.g. a gzip/brotli-encoded body that was not decompressed, or an error page in JSON/plain text).
Solutions
- Read and log a prefix of resp.Body before parsing to see what was actually received
- Do not manually set Accept-Encoding unless you handle decompression; let net/http negotiate it
- Check Content-Type of the response — ensure it is text/html
- Retry the request; a truncated body is often transient
- Verify the site still serves plain HTML rather than a JS-only shell with no HTML response
Example fix
// before
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return "", fmt.Errorf("[%s] 解析首页失败: %w", p.Name(), err)
}
// after
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
return "", fmt.Errorf("[%s] 读取首页失败: %w", p.Name(), readErr)
}
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
if err != nil {
return "", fmt.Errorf("[%s] 解析首页失败: %w (body head: %.200s)", p.Name(), err, body)
} Defensive patterns
Strategy: try-catch
Try / catch
key, err := plugin.fetchDataKey(client)
if err != nil {
if strings.Contains(err.Error(), "解析首页失败") {
// HTML parse failure: capture body sample for diagnostics, then retry once
return retryWithBodyDump(err)
}
return err
} Prevention
- Never set Accept-Encoding manually unless you decompress the body yourself
- Validate Content-Type is text/html before parsing
- Read the full body into memory first so you can log a sample on parse failure
- Retry once on transient truncation before surfacing the error
When it happens
Trigger: Response body is empty or closed mid-read, is binary/compressed content served without proper Content-Encoding handling, or the connection broke mid-body so the HTML parser hits malformed input it cannot recover from.
Common situations: Manually setting Accept-Encoding headers causing compressed bodies without decompression; a proxy returning a partial body; server streaming an error page in a non-HTML format; extremely large or truncated responses over flaky networks.
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/9ac8aed9eb2352a1.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/jsnoteclub/jsnoteclub.go:246
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://jsnoteclub.com/", nil)
if err != nil {
return "", fmt.Errorf("[%s] 创建首页请求失败: %w", p.Name(), err)
}
setHTMLHeaders(req, "https://jsnoteclub.com/")
resp, err := p.doRequestWithRetry(req, client, maxRequestRetries)
if err != nil {
return "", fmt.Errorf("[%s] 访问首页失败: %w", p.Name(), err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("[%s] 首页返回状态码: %d", p.Name(), resp.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return "", fmt.Errorf("[%s] 解析首页失败: %w", p.Name(), err)
}
var htmlBuilder strings.Builder
doc.Find("script").Each(func(_ int, s *goquery.Selection) {
if html, err := goquery.OuterHtml(s); err == nil {
htmlBuilder.WriteString(html)
}
})
match := dataKeyRegex.FindStringSubmatch(htmlBuilder.String())
if len(match) < 2 {
return "", fmt.Errorf("[%s] 未能在首页找到 data-key", p.Name())
}
return match[1], nil
}
func (p *JsNoteClubPlugin) fetchPosts(client *http.Client, dataKey string) ([]ghostPost, error) {View on GitHub (pinned to beaa561337)