fish2018/pansou · error
重试 次后仍然失败
Error message
重试 %d 次后仍然失败: %w
What it means
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.
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.
Example fix
// before
lastErr = err
}
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr)
// after
lastErr = err
time.Sleep(backoff << i) // exponential backoff between attempts
}
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr) Defensive patterns
Strategy: retry
Try / catch
var lastErr error
for i := 0; i < maxRetries; i++ {
resp, err := client.Do(req)
if err == nil { break }
lastErr = err
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
time.Sleep(time.Duration(1<<i) * time.Second) // exponential backoff
}
}
if lastErr != nil {
return fmt.Errorf("after %d retries: %w", maxRetries, lastErr)
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
Related errors
AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07).
Data as JSON: /api/errors/6dc8bbe8aba3b43d.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/duoduo/duoduo.go:399
backoff := time.Duration(1<<uint(i-1)) * 200 * time.Millisecond
time.Sleep(backoff)
}
// 克隆请求
reqClone := req.Clone(req.Context())
resp, err := client.Do(reqClone)
if err == nil && resp.StatusCode == 200 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
lastErr = err
}
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr)
}
// fetchDetailLinksAndImages 获取详情页的下载链接和图片
func (p *DuoduoAsyncPlugin) fetchDetailLinksAndImages(client *http.Client, itemID string) ([]model.Link, []string) {
// 性能统计
start := time.Now()
atomic.AddInt64(&detailPageRequests, 1)
defer func() {
duration := time.Since(start).Nanoseconds()
atomic.AddInt64(&totalDetailTime, duration)
}()
detailURL := fmt.Sprintf("https://tv.yydsys.top/index.php/vod/detail/id/%s.html", itemID)
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), DetailTimeout)
defer cancel()
View on GitHub (pinned to beaa561337)