fish2018/pansou · error
重试 次后仍然失败
Error message
重试 %d 次后仍然失败: %w
What it means
doRequestWithRetry exhausts maxRetries attempts (closing any non-nil response body each time, recording lastErr) and returns a wrapped error with the retry count. This is the aggregate failure of every attempt; the original cause is inside the wrapped lastErr (timeout, connection refused, bad status, etc.).
Solutions
- Unwrap lastErr (errors.Unwrap / %w chain) to identify the root cause — timeout vs refused vs status
- If context deadline exceeded, increase the request timeout or retry budget
- If connection refused/DNS errors, fix or update the source base URL
- If rate-limited, add backoff between retries or use a different egress IP/proxy
Example fix
// before
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr)
// after
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr) // caller: errors.Is(err, context.DeadlineExceeded) to branch Defensive patterns
Strategy: retry
Validate before calling
if err := net.DialTimeout("tcp", host+":443", 5*time.Second); err != nil {
// host unreachable; skip before invoking retry loop
} Type guard
func isRetryExhausted(err error) bool {
return strings.Contains(err.Error(), "重试") && strings.Contains(err.Error(), "仍然失败")
} Try / catch
if err != nil {
if isRetryExhausted(err) {
root := errors.Unwrap(err) // lastErr: the real cause
log.Printf("all retries failed, root cause: %v", root)
return fallbackSource.Search(ctx, kw)
}
return err
} Prevention
- Always unwrap to the root cause before reacting — the outer message only reports the retry count
- Use exponential backoff between retries instead of tight loops
- Cap total retry budget so a dead source fails fast
- Health-check sources and disable ones failing repeatedly
When it happens
Trigger: All maxRetries attempts of the HTTP request fail — every attempt returns a transport error (dial/timeout/TLS) or an error status the retry loop treats as failure, and the loop falls through to the final return.
Common situations: Upstream site down for the entire retry window, IP rate-limited or blocked so every attempt fails, DNS dead for a defunct mirror, network egress disabled in the deployment environment.
Understand the failure class
Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.
Related errors
AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07).
Data as JSON: /api/errors/61a6fbeae14d1d7a.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/kkv/kkv.go:409
for i := 0; i < maxRetries; i++ {
if i > 0 {
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)
}
View on GitHub (pinned to beaa561337)