fish2018/pansou · error

[ ] 获取内容失败

Error message

[%s] 获取内容失败: %w

What it means

fetchPosts returns this error when p.doRequestWithRetry exhausts its 3 attempts (with exponential backoff) to GET the Ghost Content API posts endpoint and every attempt failed with a transport error or a non-200 status. The underlying cause (network failure, timeout, or 'HTTP 状态码 %d') is wrapped with %w. This means the plugin could not retrieve the post list at all, so the search fails.

Solutions

  1. Test reachability of https://jsnoteclub.com/ghost/api/content/posts/ with curl using the same headers (browser User-Agent, Referer) to see the actual status code.
  2. Check network/DNS/proxy on the host running the plugin; fix connectivity or configure proxy environment correctly.
  3. Unwrap the error (errors.Unwrap) to distinguish a transport error from 'HTTP 状态码 %d'; if 403/429, the site is blocking — update User-Agent/referer headers or add longer backoff.
  4. Verify the data-key passed in fetchPosts is fresh; re-run after the posts cache (1h TTL) expires or restart to re-fetch the key.
  5. Increase maxRequestRetries or retryBaseDelay if failures are transient rate-limiting.

Example fix

// before
resp, err := p.doRequestWithRetry(req, client, maxRequestRetries)
if err != nil {
    return nil, fmt.Errorf("[%s] 获取内容失败: %w", p.Name(), err)
}
// after
resp, err := p.doRequestWithRetry(req, client, maxRequestRetries)
if err != nil {
    var statusErr error
    if errors.As(err, &statusErr) && strings.Contains(err.Error(), "状态码 403") {
        // site is blocking us — refresh headers/UA or back off
    }
    return nil, fmt.Errorf("[%s] 获取内容失败: %w", p.Name(), err)
}
Defensive patterns

Strategy: retry

Validate before calling

// check upstream reachability before invoking the plugin
resp, err := http.Head("https://jsnoteclub.com/")
if err != nil || resp.StatusCode != http.StatusOK {
    log.Printf("jsnoteclub.com unreachable, skipping: %v", err)
    return
}

Try / catch

posts, err := p.fetchPosts(client, dataKey)
if err != nil {
    var netErr net.Error
    if errors.As(err, &netErr) && netErr.Timeout() {
        // transient — schedule retry with backoff
    }
    return nil, fmt.Errorf("upstream unavailable: %w", err)
}

Prevention

When it happens

Trigger: All 3 attempts of client.Do inside doRequestWithRetry fail: DNS resolution failure, connection refused/reset, TLS errors, the 12s requestTimeout context expiring, or persistent non-200 responses (e.g. 403/429/5xx) from https://jsnoteclub.com/ghost/api/content/posts/.

Common situations: Server offline or DNS broken; the site rate-limiting or blocking the plugin's IP/User-Agent (Cloudflare challenges returning 403); a data-key that has been rotated making the API reject the request; corporate proxy/firewall blocking outbound HTTPS; transient network outage during all retries.

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/a94e6b10b973f950. Report an issue: GitHub.

Appendix: source

Thrown at plugin/jsnoteclub/jsnoteclub.go:284

	params.Set("key", dataKey)
	params.Set("limit", "10000")
	params.Set("fields", "id,slug,title,excerpt,url,updated_at,visibility")
	params.Set("order", "updated_at DESC")

	reqURL := fmt.Sprintf("https://jsnoteclub.com/ghost/api/content/posts/?%s", params.Encode())

	ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
	if err != nil {
		return nil, fmt.Errorf("[%s] 创建内容请求失败: %w", p.Name(), err)
	}
	setAPIHeaders(req, "https://jsnoteclub.com/")

	resp, err := p.doRequestWithRetry(req, client, maxRequestRetries)
	if err != nil {
		return nil, fmt.Errorf("[%s] 获取内容失败: %w", p.Name(), err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("[%s] 内容接口返回状态码: %d", p.Name(), resp.StatusCode)
	}

	var payload ghostPostsResponse
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return nil, fmt.Errorf("[%s] 解析内容数据失败: %w", p.Name(), err)
	}

	return payload.Posts, nil
}

func (p *JsNoteClubPlugin) fetchDetailLinks(client *http.Client, detailURL string) []model.Link {
	if cached, ok := detailCache.Load(detailURL); ok {
		if entry, valid := cached.(detailCacheEntry); valid && time.Now().Before(entry.expiresAt) {

View on GitHub (pinned to beaa561337)