{"record":{"id":"b5a7a8c3266c290b","repo":"fish2018/pansou","slug":"s-html-w-b5a7a8","errorCode":null,"errorMessage":"[%s] HTML解析失败: %w","messagePattern":"\\[(.+?)\\] HTML解析失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/kkv/kkv.go","lineNumber":142,"sourceCode":"\t}\n\t\n\tp.setHeaders(req, baseURL)\n\t\n\tresp, err := p.doRequestWithRetry(req, client)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[%s] 搜索请求失败: %w\", p.Name(), err)\n\t}\n\tdefer resp.Body.Close()\n\t\n\tdebugPrintf(\"📡 HTTP状态码: %d\\n\", resp.StatusCode)\n\t\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"[%s] 请求返回状态码: %d\", p.Name(), resp.StatusCode)\n\t}\n\t\n\tdoc, err := goquery.NewDocumentFromReader(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[%s] HTML解析失败: %w\", p.Name(), err)\n\t}\n\t\n\tvar items []searchItem\n\tdoc.Find(\"article.post\").Each(func(i int, s *goquery.Selection) {\n\t\tlink := s.Find(\".entry-header h2.entry-title a\")\n\t\thref, exists := link.Attr(\"href\")\n\t\tif !exists {\n\t\t\tdebugPrintf(\"⚠️ 第%d个结果没有href属性\\n\", i+1)\n\t\t\treturn\n\t\t}\n\t\t\n\t\ttitle := strings.TrimSpace(link.Text())\n\t\tif title == \"\" {\n\t\t\tdebugPrintf(\"⚠️ 第%d个结果标题为空\\n\", i+1)\n\t\t\treturn\n\t\t}\n\t\t\n\t\tre := regexp.MustCompile(`\\?p=(\\d+)`)","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/kkv/kkv.go#L124-L160","documentation":"goquery.NewDocumentFromReader failed to parse the kkv search response body as HTML. This happens when the 200 response is not valid parseable HTML — e.g. a compressed/binary body, truncated response, or a challenge page that breaks the parser.","triggerScenarios":"goquery.NewDocumentFromReader(resp.Body) returns err after a 200 status: body is gzip-encoded but not auto-decompressed, connection truncated mid-body, or the body is JSON/empty instead of HTML.","commonSituations":"A custom http.Client without automatic gzip handling receiving encoded bytes, a proxy returning an error page or empty 200 body, the site serving a Cloudflare challenge that goquery chokes on.","solutions":["Check the wrapped parse error; if it mentions gzip/zlib, let net/http auto-decompress by not setting Accept-Encoding manually","Dump the first bytes of resp.Body to confirm the 200 response is actually HTML and not empty or a challenge page","Check the proxy/CDN in front of the scraper isn't corrupting or truncating bodies","Retry the request if the body was truncated by a flaky connection (doRequestWithRetry already retries transport errors, but not short bodies)"],"exampleFix":"// before\nreq.Header.Set(\"Accept-Encoding\", \"gzip, deflate\")\n// after\n// omit Accept-Encoding so net/http transparently decompresses for goquery","handlingStrategy":"validation","validationCode":"head, _ := client.Head(searchURL)\nct := head.Header.Get(\"Content-Type\")\nif !strings.Contains(ct, \"text/html\") {\n    // expect HTML parse failure; skip source\n}","typeGuard":"func looksLikeHTML(b []byte) bool {\n    s := strings.TrimSpace(strings.ToLower(string(b[:min(len(b), 512)])))\n    return strings.HasPrefix(s, \"<!doctype html\") || strings.HasPrefix(s, \"<html\")\n}","tryCatchPattern":"if err != nil {\n    if strings.Contains(err.Error(), \"HTML解析失败\") {\n        log.Printf(\"non-HTML 200 body from source, skipping: %v\", err)\n        return nil\n    }\n    return err\n}","preventionTips":["Never set Accept-Encoding manually; let net/http decompress","Check Content-Type of upstream responses before parsing","Beware proxies/CDNs that return empty or challenge 200 bodies","Sniff the first bytes when debugging parse errors"],"tags":["html","parsing","goquery","scraping"],"backgroundTag":"invalid-html-response","analyzedSha":"beaa56133755a548ebc51b090b3816e2ae044aa6","analyzedAt":"2026-09-07T00:31:18.025Z","contentChangedAt":"2026-09-07T00:31:18.025Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}