fish2018/pansou · error

重试 次后失败

Error message

重试 %d 次后失败: %w

What it means

This error is returned by doRequestWithRetry after exhausting maxRetries attempts to fetch remote data. lastErr is wrapped with %w so the underlying cause (timeout, connection failure, non-2xx, etc.) is preserved. The plugin's callers fetchDataKey, fetchPosts, and fetchDetailLinks all surface it when the upstream source is unreachable or persistently failing.

Solutions

  1. Test connectivity to the upstream host from the deployment machine (curl the source URL) to confirm the network path works.
  2. Check the wrapped lastErr chain (errors.Unwrap / %v of the message) to identify the root cause — timeout vs status code vs DNS.
  3. Increase maxRetries or retryBaseDelay if the upstream is intermittently slow.
  4. Configure an HTTP proxy if the host requires one to be reachable.
  5. Verify the source URL is still valid; upstream APIs change or shut down over time.

Example fix

// before: retries fail with no diagnosis
result, err := fetchPosts(client)
// after: inspect the wrapped cause
if err != nil {
    log.Printf("fetchPosts failed: %v", errors.Unwrap(err))
    // e.g. context deadline exceeded -> raise requestTimeout or add proxy
}
Defensive patterns

Strategy: retry

Validate before calling

url, err := url.Parse(sourceURL)
if err != nil || url == nil || url.Host == "" { return err }
conn, err := net.DialTimeout("tcp", net.JoinHostPort(url.Hostname(), portOr(url, "443")), 3*time.Second)
if err != nil { return fmt.Errorf("upstream unreachable: %w", err) }
conn.Close()

Try / catch

results, err := fetchPosts(client)
if err != nil {
    var retryErr = err
    for unwrapped := errors.Unwrap(err); unwrapped != nil; unwrapped = errors.Unwrap(unwrapped) {
        retryErr = unwrapped
    }
    log.Printf("upstream fetch failed after retries, root cause: %v", retryErr)
    results = fallbackResults // serve cached/stale data
}

Prevention

When it happens

Trigger: Any of fetchDataKey, fetchPosts, or fetchDetailLinks calling doRequestWithRetry while the remote endpoint keeps failing on every attempt (network errors, timeouts, repeated bad status codes), after exponential backoff sleeps of retryBaseDelay<<attempt between attempts.

Common situations: Upstream site is down or blocked (e.g. by GFW or geo-restriction), DNS failure in the deployment environment, no outbound internet in a container, or the source raising rate limits that persist across all retries.

Understand the failure class

Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.

Related errors


AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07). Data as JSON: /api/errors/38e7c1f178a94899. Report an issue: GitHub.

Appendix: source

Thrown at plugin/jsnoteclub/jsnoteclub.go:516

	for attempt := 0; attempt < maxRetries; attempt++ {
		resp, err := client.Do(req.Clone(req.Context()))
		if err == nil && resp.StatusCode == http.StatusOK {
			return resp, nil
		}
		if resp != nil {
			resp.Body.Close()
		}
		lastErr = err
		if err == nil {
			lastErr = fmt.Errorf("HTTP 状态码 %d", resp.StatusCode)
		}
		if attempt < maxRetries-1 {
			time.Sleep(retryBaseDelay * time.Duration(1<<attempt))
		}
	}

	return nil, fmt.Errorf("重试 %d 次后失败: %w", maxRetries, lastErr)
}

func newHTTPClient() *http.Client {
	return &http.Client{
		Timeout: requestTimeout,
		Transport: &http.Transport{
			MaxIdleConns:        httpMaxIdleConns,
			MaxIdleConnsPerHost: httpMaxIdlePerHost,
			MaxConnsPerHost:     httpMaxConnsPerHost,
			IdleConnTimeout:     90 * time.Second,
			TLSHandshakeTimeout: 10 * time.Second,
		},
	}
}

func setHTMLHeaders(req *http.Request, referer string) {
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")

View on GitHub (pinned to beaa561337)