{"record":{"id":"623d27284779b29b","repo":"fish2018/pansou","slug":"d-w-623d27","errorCode":null,"errorMessage":"重试 %d 次后仍然失败: %w","messagePattern":"重试 (.+?) 次后仍然失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/yunsou/yunsou.go","lineNumber":156,"sourceCode":"\nfunc (p *YunsouAsyncPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {\n\tvar lastErr error\n\tfor attempt := 0; attempt < maxRetries; attempt++ {\n\t\tif attempt > 0 {\n\t\t\ttime.Sleep(time.Duration(1<<(attempt-1)) * 200 * time.Millisecond)\n\t\t}\n\t\tresp, err := client.Do(req.Clone(req.Context()))\n\t\tif err == nil && resp.StatusCode == http.StatusOK {\n\t\t\treturn resp, nil\n\t\t}\n\t\tif resp != nil {\n\t\t\tlastErr = fmt.Errorf(\"状态码 %d\", resp.StatusCode)\n\t\t\tresp.Body.Close()\n\t\t} else {\n\t\t\tlastErr = err\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"重试 %d 次后仍然失败: %w\", maxRetries, lastErr)\n}\n\nfunc (p *YunsouAsyncPlugin) parseSearchResults(doc *goquery.Document) []model.SearchResult {\n\tresults := make([]model.SearchResult, 0)\n\tdoc.Find(\".list .item\").Each(func(_ int, item *goquery.Selection) {\n\t\tif len(results) >= maxResults {\n\t\t\treturn\n\t\t}\n\t\ttitle, _ := item.Attr(\"data-title\")\n\t\tif title == \"\" {\n\t\t\ttitle = item.Find(\".title\").First().Text()\n\t\t}\n\t\ttitle = cleanText(html.UnescapeString(title))\n\t\tif title == \"\" {\n\t\t\treturn\n\t\t}\n\n\t\tshareURL, password := \"\", \"\"","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/yunsou/yunsou.go#L138-L174","documentation":"This final error from yunsou's doRequestWithRetry means every one of maxRetries attempts failed — either with a network error or a non-200 status (the last failure is wrapped via %w). The page fetch is abandoned and fetchPage surfaces it as '第%d页搜索请求失败'. It is the plugin's definitive 'upstream unreachable or rejecting us' signal.","triggerScenarios":"Loop in doRequestWithRetry completes with resp==nil or status!=200 on all attempts; lastErr (network error or '状态码 %d') is wrapped and returned to fetchPage.","commonSituations":"Sustained site outage, IP blocked by the site's firewall, no internet connection, or DNS misconfiguration in a container/CI environment.","solutions":["Read the wrapped lastErr to distinguish network failure vs bad status code.","Test reachability manually with curl using the same headers.","Increase maxRetries and add exponential backoff between attempts.","If IP is blocked, use a proxy or wait for the block to expire.","Fail gracefully: skip the failing page instead of aborting the entire search."],"exampleFix":"// before\nreturn nil, fmt.Errorf(\"重试 %d 次后仍然失败: %w\", maxRetries, lastErr)\n// after\nfor attempt := 1; attempt <= maxRetries; attempt++ {\n    ...\n    time.Sleep(time.Duration(attempt) * time.Second) // exponential backoff\n}\nreturn nil, fmt.Errorf(\"重试 %d 次后仍然失败: %w\", maxRetries, lastErr)","handlingStrategy":"retry","validationCode":"func siteUp(u string) bool {\n    c := &http.Client{Timeout: 5 * time.Second}\n    resp, err := c.Head(u)\n    return err == nil && resp.StatusCode < 500\n}","typeGuard":null,"tryCatchPattern":"results, err := plugin.Search(keyword)\nif err != nil {\n    var retried *RetryExhaustedError\n    if errors.As(err, &retried) {\n        return fallbackPlugin.Search(keyword) // switch to another plugin/mirror\n    }\n    return nil, err\n}","preventionTips":["Pre-verify mirror availability before searching","Use exponential backoff, not tight retry loops","Keep multiple mirrors configured for fallback","Alert on sustained retry exhaustion per host"],"tags":["network","retry-exhausted","http","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"}