{"record":{"id":"d9500b220f171cae","repo":"fish2018/pansou","slug":"s-w-d9500b","errorCode":null,"errorMessage":"[%s] 解析搜索结果失败: %w","messagePattern":"\\[(.+?)\\] 解析搜索结果失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/duanjuw/duanjuw.go","lineNumber":126,"sourceCode":"\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[%s] 创建搜索请求失败: %w\", p.Name(), err)\n\t}\n\tsetDuanjuwHeaders(req, duanjuwBaseURL+\"/\")\n\n\tresp, err := doDuanjuwRequestWithRetry(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\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"[%s] 搜索返回状态码: %d\", p.Name(), resp.StatusCode)\n\t}\n\n\tdoc, err := goquery.NewDocumentFromReader(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[%s] 解析搜索结果失败: %w\", p.Name(), err)\n\t}\n\n\titems := p.parseSearchResults(doc)\n\tif len(items) == 0 {\n\t\treturn []model.SearchResult{}, nil\n\t}\n\t// The current site renders search results as numbered chat entries with\n\t// direct pan links. Older pages still use result cards and need detail fetches.\n\tfor _, item := range items {\n\t\tif len(item.Links) > 0 {\n\t\t\treturn plugin.FilterResultsByKeyword(items, keyword), nil\n\t\t}\n\t}\n\n\tresults := p.enrichResults(client, items)\n\treturn plugin.FilterResultsByKeyword(results, keyword), nil\n}\n","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/duanjuw/duanjuw.go#L108-L144","documentation":"The duanjuw plugin wraps goquery.NewDocumentFromReader failures as '[plugin] failed to parse search results'. The body could not be parsed as an HTML document, usually because it is empty or not HTML despite a 200 status.","triggerScenarios":"goquery.NewDocumentFromReader(resp.Body) returns err in searchImpl — empty body, truncated stream, or non-HTML content (binary, badly encoded).","commonSituations":"Server returns 200 with an empty or challenge body; compression mismatch when Accept-Encoding is set manually; connection cut mid-body causing malformed HTML.","solutions":["Read and log the first bytes of resp.Body on failure to see the actual content","Avoid manually setting Accept-Encoding unless the Transport decompresses","Retry — empty 200 bodies are often transient","If the site changed its page structure/content type, update the parser"],"exampleFix":"// before\ndoc, err := goquery.NewDocumentFromReader(resp.Body)\nif err != nil { return nil, err }\n// after\nbody, rerr := io.ReadAll(resp.Body)\nif rerr != nil || len(bytes.TrimSpace(body)) == 0 {\n    return nil, fmt.Errorf(\"empty search body\")\n}\ndoc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))","handlingStrategy":"validation","validationCode":"body, _ := io.ReadAll(resp.Body)\nif len(bytes.TrimSpace(body)) == 0 {\n    return fmt.Errorf(\"empty body; skip goquery parse\")\n}\nif !utf8.Valid(body) {\n    return fmt.Errorf(\"non-utf8 body\")\n}","typeGuard":null,"tryCatchPattern":"doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))\nif err != nil {\n    log.Printf(\"parse failed, body head: %q\", body[:min(200,len(body))])\n    return []model.SearchResult{}, nil\n}","preventionTips":["Buffer the body first so it can be inspected and re-read","Never set Accept-Encoding without transport decompression","Retry empty/truncated 200 responses","Test the parser against saved snapshots when the site updates"],"tags":["html","parsing","goquery"],"backgroundTag":"unexpected-api-response-shape","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"}