fish2018/pansou · error
重试 次后仍然失败
Error message
重试 %d 次后仍然失败: %w
What it means
CldiPlugin.doRequestWithRetry exhausts its full retry budget (maxRetries attempts) without ever getting a successful HTTP response. It keeps the last failure in lastErr and returns a wrapped error combining the retry count with the final underlying cause (timeout, connection reset, TLS error, etc.). Callers like searchPage only see this aggregate error, so the root cause is inside the %w chain.
Solutions
- Inspect the wrapped lastErr (errors.Unwrap / %v of the returned error) to identify the real cause — timeout vs connection refused vs TLS — before changing code.
- Verify basic connectivity to the search endpoint with curl from the same host.
- Increase maxRetries or the per-request timeout if the site is slow but reachable.
- Add exponential backoff between attempts (currently linear/sleep-based) and respect Retry-After headers if the site rate-limits.
- Check for anti-bot measures; update request headers (User-Agent, cookies) in the plugin if the site started blocking default clients.
Example fix
// before
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr)
// after
if errors.Is(lastErr, context.DeadlineExceeded) {
TimeoutSeconds *= 2 // or make it configurable
}
return nil, fmt.Errorf("cldi: request failed after %d retries: %w", maxRetries, lastErr) Defensive patterns
Strategy: retry
Validate before calling
// caller-side reachability probe before invoking the plugin
func reachable(rawURL string) bool {
resp, err := http.Head(rawURL)
return err == nil && resp != nil
} Try / catch
results, err := plugin.Search(ctx, keyword)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() { /* increase timeout, retry */ }
log.Printf("cldi search failed after retries: %v", err)
return fallbackResults
} Prevention
- Pre-check endpoint reachability before batch searches.
- Configure an adequate per-request timeout; a too-short timeout makes all retries fail identically.
- Use jittered exponential backoff and honor rate-limit signals.
- Monitor the target site for anti-bot changes; keep headers current.
- Keep the wrapped lastErr in logs — always use %w, not %v.
When it happens
Trigger: p.searchPage -> p.doRequestWithRetry makes maxRetries HTTP GET attempts to the CLDI search endpoint and every attempt fails (network unreachable, per-request context deadline expired, connection reset by peer, DNS failure). After the loop, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr) is returned.
Common situations: Target site is down or blocking the scraper (rate limiting, WAF/anti-bot dropping connections); the host has no outbound network or DNS fails; TimeoutSeconds is too short for a slow endpoint; a misconfigured proxy makes all attempts fail identically.
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/fab7dc4c7237add0.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/cldi/cldi.go:207
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)
}
// extractSearchResults 提取搜索结果
func (p *CldiPlugin) extractSearchResults(doc *goquery.Document) []model.SearchResult {
var results []model.SearchResult
// New cldi releases use article.resource cards and expose the BT hash in
// /hash/<40-hex>.html links. The hash itself is a valid magnet identifier,
// so no browser-only "copy magnet" action is required.
doc.Find("article.resource").Each(func(_ int, article *goquery.Selection) {
anchor := article.Find("h2 a[href]").First()
href, _ := anchor.Attr("href")
href = strings.TrimSpace(href)
match := hashPathRegex.FindStringSubmatch(href)
if len(match) < 2 {
return
}
title := p.cleanTitle(anchor.Text())View on GitHub (pinned to beaa561337)