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

  1. 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.
  2. Verify basic connectivity to the search endpoint with curl from the same host.
  3. Increase maxRetries or the per-request timeout if the site is slow but reachable.
  4. Add exponential backoff between attempts (currently linear/sleep-based) and respect Retry-After headers if the site rate-limits.
  5. 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

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)