{"record":{"id":"f4648ac0a18cb34e","repo":"fish2018/pansou","slug":"s-html-w-f4648a","errorCode":null,"errorMessage":"[%s] 解析搜索结果HTML失败: %w","messagePattern":"\\[(.+?)\\] 解析搜索结果HTML失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/diduan/diduan.go","lineNumber":181,"sourceCode":"// executeSearch 执行搜索请求\nfunc (p *DiduanPlugin) executeSearch(keyword string) ([]model.SearchResult, error) {\n\t// 构建搜索URL\n\tsearchURL := fmt.Sprintf(\"%s%s\", BaseURL, fmt.Sprintf(SearchPath, url.QueryEscape(keyword)))\n\n\tresp, err := p.getPage(searchURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[%s] 搜索请求失败: %w\", p.Name(), err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != 200 {\n\t\treturn nil, p.httpStatusError(\"搜索\", resp)\n\t}\n\n\t// 解析HTML提取搜索结果\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\n\treturn p.parseSearchResults(doc)\n}\n\n// getPage 串行化 cloudscraper 调用，避免其 stealth 计数器并发竞争。\nfunc (p *DiduanPlugin) getPage(rawURL string) (*http.Response, error) {\n\tp.scraperMu.Lock()\n\tdefer p.scraperMu.Unlock()\n\treturn p.scraper.Get(rawURL)\n}\n\nfunc (p *DiduanPlugin) httpStatusError(action string, resp *http.Response) error {\n\tif strings.EqualFold(resp.Header.Get(\"cf-mitigated\"), \"challenge\") {\n\t\treturn fmt.Errorf(\"[%s] %s触发 Cloudflare Managed Challenge (HTTP %d)\", p.Name(), action, resp.StatusCode)\n\t}\n\treturn fmt.Errorf(\"[%s] %sHTTP状态错误: %d\", p.Name(), action, resp.StatusCode)\n}","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/diduan/diduan.go#L163-L199","documentation":"After a successful page fetch, executeSearch parses the response body with goquery.NewDocumentFromReader. If constructing the document fails (I/O error while reading the body, malformed stream), the error is wrapped with this message and the plugin name. This indicates the search response could not be turned into a parseable HTML document.","triggerScenarios":"goquery.NewDocumentFromReader(resp.Body) returns an error — typically a read failure on the response body stream (connection closed mid-body, decompression error) rather than invalid HTML, since goquery tolerates most markup.","commonSituations":"Server closes connection while streaming the body; gzip/brotli decompression mismatch because Accept-Encoding and actual encoding diverge; an interrupted proxy connection delivering a truncated response.","solutions":["Unwrap the error to see the underlying body-read failure.","Check Content-Encoding handling — ensure the scraper/client negotiates and decompresses encodings correctly.","Retry the request; truncated bodies are usually transient.","Capture the raw response (status, length) when it happens to diagnose whether the server sent a valid body."],"exampleFix":"// before\ndoc, err := goquery.NewDocumentFromReader(resp.Body)\nif err != nil { return err }\n// after\ndoc, err := goquery.NewDocumentFromReader(resp.Body)\nif err != nil {\n    return fmt.Errorf(\"bad search response (status=%d): %w\", resp.StatusCode, err)\n}","handlingStrategy":"retry","validationCode":"if resp.ContentLength == 0 {\n    return errors.New(\"empty search response body, skipping parse\")\n}","typeGuard":null,"tryCatchPattern":"doc, err := goquery.NewDocumentFromReader(resp.Body)\nif err != nil {\n    return fmt.Errorf(\"unreadable response (status=%d): %w\", resp.StatusCode, err) // caller retries\n}","preventionTips":["Retry truncated-body failures; they are usually transient.","Verify Content-Encoding negotiation matches the client's decompressor.","Log status and content length whenever parsing fails."],"tags":["html","parsing","goquery","go"],"backgroundTag":"invalid-json-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"}