{"record":{"id":"3a56d01a0882f09d","repo":"fish2018/pansou","slug":"d-w-3a56d0","errorCode":null,"errorMessage":"重试 %d 次后仍然失败: %w","messagePattern":"重试 (.+?) 次后仍然失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/pianku/pianku.go","lineNumber":221,"sourceCode":"\t\t\tbackoff := time.Duration(1<<uint(i-1)) * 200 * time.Millisecond\n\t\t\ttime.Sleep(backoff)\n\t\t}\n\t\t\n\t\t// 克隆请求避免并发问题\n\t\treqClone := req.Clone(req.Context())\n\t\t\n\t\tresp, err := client.Do(reqClone)\n\t\tif err == nil && resp.StatusCode == 200 {\n\t\t\treturn resp, nil\n\t\t}\n\t\t\n\t\tif resp != nil {\n\t\t\tresp.Body.Close()\n\t\t}\n\t\tlastErr = err\n\t}\n\t\n\treturn nil, fmt.Errorf(\"重试 %d 次后仍然失败: %w\", MaxRetries, lastErr)\n}\n\n// extractSearchResults 提取搜索结果\nfunc (p *PiankuPlugin) extractSearchResults(doc *goquery.Document) []model.SearchResult {\n\tvar results []model.SearchResult\n\t\n\t// 查找搜索结果容器\n\tdoc.Find(\".sr_lists dl\").Each(func(i int, s *goquery.Selection) {\n\t\tresult := p.extractSingleResult(s)\n\t\tif result.UniqueID != \"\" && len(result.Links) > 0 {\n\t\t\tresults = append(results, result)\n\t\t}\n\t})\n\t\n\treturn results\n}\n\n// extractSingleResult 提取单个搜索结果","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/pianku/pianku.go#L203-L239","documentation":"Pianku plugin's doRequestWithRetry wraps the last error after exhausting MaxRetries HTTP attempts. It signals that every retry of the request failed (network errors, timeouts, or non-recoverable transport failures), and the final underlying error is preserved via %w. Callers searchImpl and fetchDetailPageLinks propagate this up as a failed search.","triggerScenarios":"Any request issued through doRequestWithRetry (from searchImpl or fetchDetailPageLinks) where all MaxRetries attempts fail with a transport-level error: connection reset, TLS handshake failure, DNS resolution failure, or context deadline exceeded inside the retry loop.","commonSituations":"Site is down or blocking the scraper (rate limiting, IP ban); corporate proxy/firewall dropping HTTPS; slow network causing per-attempt timeouts; the site changed its TLS config so the client handshake fails repeatedly.","solutions":["Check basic connectivity to the target host (curl -v the search URL) to see if the site is reachable from this machine.","Increase MaxRetries and/or the per-request timeout constants in plugin/pianku/pianku.go if failures are transient slowness.","Inspect the wrapped lastErr (errors.Unwrap / %v) to identify the root cause — timeout vs refused vs TLS — and fix accordingly.","Configure a working HTTP proxy for the client if the host is blocked from this network, or rotate exit IPs if rate-limited."],"exampleFix":"// before\nreturn nil, fmt.Errorf(\"重试 %d 次后仍然失败: %w\", MaxRetries, lastErr)\n// after\nvar netErr net.Error\nif errors.As(lastErr, &netErr) && netErr.Timeout() {\n    return nil, fmt.Errorf(\"重试 %d 次后仍然失败(超时): %w\", MaxRetries, lastErr)\n}\nreturn nil, fmt.Errorf(\"重试 %d 次后仍然失败: %w\", MaxRetries, lastErr)","handlingStrategy":"retry","validationCode":"// pre-check reachability before invoking the plugin search\nresp, err := http.Head(\"https://www.pianku.tv\")\nif err != nil || resp.StatusCode >= 500 {\n    log.Println(\"pianku site unreachable, skipping search\")\n    return\n}","typeGuard":"func isRetryExhausted(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"重试\")\n}","tryCatchPattern":"results, err := plugin.Search(keyword)\nif err != nil {\n    var netErr net.Error\n    if errors.As(err, &netErr) && netErr.Timeout() {\n        // retry later with backoff\n    } else {\n        log.Printf(\"pianku search failed permanently: %v\", err)\n    }\n    return fallbackResults, nil\n}","preventionTips":["Check site reachability before running bulk searches.","Tune MaxRetries and timeouts for your network conditions.","Inspect the wrapped cause with errors.Unwrap to route handling.","Keep multiple plugin sources as fallbacks so one dead site doesn't break the app."],"tags":["network","retry-exhausted","http-client","scraping"],"backgroundTag":"retry-exhausted","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"}