{"record":{"id":"6dc8bbe8aba3b43d","repo":"fish2018/pansou","slug":"d-w-6dc8bb","errorCode":null,"errorMessage":"重试 %d 次后仍然失败: %w","messagePattern":"重试 (.+?) 次后仍然失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/duoduo/duoduo.go","lineNumber":399,"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// fetchDetailLinksAndImages 获取详情页的下载链接和图片\nfunc (p *DuoduoAsyncPlugin) fetchDetailLinksAndImages(client *http.Client, itemID string) ([]model.Link, []string) {\n\t// 性能统计\n\tstart := time.Now()\n\tatomic.AddInt64(&detailPageRequests, 1)\n\tdefer func() {\n\t\tduration := time.Since(start).Nanoseconds()\n\t\tatomic.AddInt64(&totalDetailTime, duration)\n\t}()\n\n\tdetailURL := fmt.Sprintf(\"https://tv.yydsys.top/index.php/vod/detail/id/%s.html\", itemID)\n\n\t// 创建带超时的上下文\n\tctx, cancel := context.WithTimeout(context.Background(), DetailTimeout)\n\tdefer cancel()\n","sourceCodeStart":381,"sourceCodeEnd":417,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/duoduo/duoduo.go#L381-L417","documentation":"doRequestWithRetry exhausted maxRetries attempts, each returning an error, and gives up wrapping the last error with the retry count. This is the aggregation point for all transport failures in the duoduo plugin's HTTP calls, used by both searchImpl and fetchDetailLinksAndImages. The root cause (net.Error, timeout, etc.) is chained via %w in lastErr.","triggerScenarios":"All retry attempts of client.Do(req) fail — persistent DNS failure, connection refused/reset, TLS handshake error, or every attempt exceeding the request context deadline set by DefaultTimeout.","commonSituations":"The site is down or the domain has expired; a corporate firewall blocks the outbound request; the proxy configured on the http.Client is dead; the timeout is too short for a slow/blocked route so every retry also times out.","solutions":["Unwrap and inspect lastErr (errors.Is/As for context.DeadlineExceeded, *net.OpError, x509 errors) to find the real cause.","Verify reachability with curl -v against the target URL from the same host.","Increase DefaultTimeout and/or maxRetries with exponential backoff between attempts.","If the site is persistently unreachable, mark the plugin/source unhealthy and fall back to another source instead of hammering retries."],"exampleFix":"// before\nlastErr = err\n}\nreturn nil, fmt.Errorf(\"重试 %d 次后仍然失败: %w\", maxRetries, lastErr)\n// after\nlastErr = err\ntime.Sleep(backoff << i) // exponential backoff between attempts\n}\nreturn nil, fmt.Errorf(\"重试 %d 次后仍然失败: %w\", maxRetries, lastErr)","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"var lastErr error\nfor i := 0; i < maxRetries; i++ {\n    resp, err := client.Do(req)\n    if err == nil { break }\n    lastErr = err\n    var netErr net.Error\n    if errors.As(err, &netErr) && netErr.Timeout() {\n        time.Sleep(time.Duration(1<<i) * time.Second) // exponential backoff\n    }\n}\nif lastErr != nil {\n    return fmt.Errorf(\"after %d retries: %w\", maxRetries, lastErr)\n}","preventionTips":["Use exponential backoff, not tight retry loops.","Keep the request context timeout longer than the sum of retries needs.","Classify errors: retry only transient (timeout, reset, 5xx).","Surface the wrapped root cause, not just the retry count."],"tags":["network","retry","timeout","http"],"backgroundTag":"request-timeout","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"}