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
- 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.
- Check network/DNS/proxy on the host running the plugin; fix connectivity or configure proxy environment correctly.
- 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.
- 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.
- 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
- Monitor jsnoteclub.com availability; treat this error as upstream downtime rather than a local bug.
- Keep the built-in retry (3 attempts, exponential backoff) and consider raising it for flaky networks.
- Send browser-like headers (User-Agent, Referer) to avoid bot blocks, as setAPIHeaders does.
- Cache successful post lists (plugin already caches 1h) so transient failures don't break searches.
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)