{"record":{"id":"0239d478eca6a695","repo":"fish2018/pansou","slug":"w-0239d4","errorCode":null,"errorMessage":"详情页请求失败: %w","messagePattern":"详情页请求失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/pianku/pianku.go","lineNumber":393,"sourceCode":"// fetchDetailPageLinks 获取详情页的下载链接\nfunc (p *PiankuPlugin) fetchDetailPageLinks(client *http.Client, detailURL string) ([]model.Link, error) {\n\t// 创建带超时的上下文\n\tctx, cancel := context.WithTimeout(context.Background(), TimeoutSeconds*time.Second)\n\tdefer cancel()\n\t\n\t// 创建请求\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", detailURL, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"创建详情页请求失败: %w\", err)\n\t}\n\t\n\t// 设置请求头\n\tp.setRequestHeaders(req)\n\t\n\t// 发送HTTP请求\n\tresp, err := p.doRequestWithRetry(req, client)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"详情页请求失败: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\t\n\t// 检查状态码\n\tif resp.StatusCode != 200 {\n\t\treturn nil, fmt.Errorf(\"详情页请求返回状态码: %d\", resp.StatusCode)\n\t}\n\t\n\t// 解析HTML\n\tdoc, err := goquery.NewDocumentFromReader(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"详情页HTML解析失败: %w\", err)\n\t}\n\t\n\t// 提取下载链接\n\treturn p.extractDownloadLinks(doc), nil\n}\n","sourceCodeStart":375,"sourceCodeEnd":411,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/pianku/pianku.go#L375-L411","documentation":"fetchDetailPageLinks wraps any error returned by doRequestWithRetry for the detail-page request. All retry attempts for fetching the detail page failed at the transport level (connection, timeout, TLS), and the wrapped retry-exhaustion error is included via %w.","triggerScenarios":"p.doRequestWithRetry(req, client) returns err after MaxRetries failed attempts on the detail URL — site unreachable, request context (30s) deadline exceeded, or connection reset on every try.","commonSituations":"Anti-bot protection (403/connection drop on detail pages but search works); detail pages on a different CDN host that is blocked; network outage mid-search while iterating detail links.","solutions":["Unwrap and inspect the root error to distinguish timeout vs connection-refused vs TLS failure.","Retry with a fresh context / longer timeout; the 30-second cap may be too short for slow detail pages.","Verify the detail host is reachable (curl the exact detailURL) and adjust proxy settings if blocked.","Consider treating per-link failures as non-fatal: skip the failing detail page instead of aborting the whole search."],"exampleFix":"// before\nresp, err := p.doRequestWithRetry(req, client)\nif err != nil {\n    return nil, fmt.Errorf(\"详情页请求失败: %w\", err)\n}\n// after\nresp, err := p.doRequestWithRetry(req, client)\nif err != nil {\n    log.Printf(\"详情页 %s 请求失败，跳过: %v\", detailURL, err)\n    return nil, nil // skip this detail page, continue search\n}","handlingStrategy":"fallback","validationCode":"conn, err := net.DialTimeout(\"tcp\", host+\":443\", 5*time.Second)\nif err != nil {\n    log.Println(\"detail host unreachable, skip\")\n    return\n}\nconn.Close()","typeGuard":"func isTransportError(err error) bool {\n    var urlErr *url.Error\n    return errors.As(err, &urlErr)\n}","tryCatchPattern":"links, err := plugin.Search(keyword)\nif err != nil {\n    if strings.Contains(err.Error(), \"详情页请求失败\") {\n        // per-link failure: degrade instead of aborting\n        return partialResults, nil\n    }\n    return nil, err\n}","preventionTips":["Treat individual detail-page failures as skippable, not fatal.","Use adequate timeouts for slow detail pages.","Send full browser headers to avoid connection-level bot blocking.","Monitor which hosts fail persistently and cache the unreachability."],"tags":["network","retry-exhausted","http-client","scraping"],"backgroundTag":"http-request-failed","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"}