fish2018/pansou · error
[ ] 解析内容数据失败
Error message
[%s] 解析内容数据失败: %w
What it means
fetchPosts returns this error when json.NewDecoder(resp.Body).Decode(&payload) fails to parse the HTTP 200 response body into ghostPostsResponse ({"posts":[...]}). This means the server returned 200 but the body is not the expected JSON — commonly an HTML error/challenge page, truncated body, or a changed API schema. The JSON syntax error is wrapped with %w.
Solutions
- Log the first bytes of resp.Body (Content-Type and a body snippet) when this error occurs to confirm whether the 200 body is HTML instead of JSON.
- If the body is an HTML challenge page, improve headers (setAPIHeaders) or add cookie/TLS fingerprinting to get past bot protection.
- Check whether the context deadline is too short — a truncated read surfaces as an unexpected EOF; requestTimeout is 12s.
- Compare the actual response against the expected {"posts":[...]} schema and update the ghostPostsResponse/ghostPost struct tags if the site's API changed.
- Verify no intermediary proxy is mangling/compressing the response; test with curl using identical headers.
Example fix
// before
var payload ghostPostsResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, fmt.Errorf("[%s] 解析内容数据失败: %w", p.Name(), err)
}
// after
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
var payload ghostPostsResponse
if err := json.Unmarshal(body, &payload); err != nil {
return nil, fmt.Errorf("[%s] 解析内容数据失败: %w (body starts with: %.100s)", p.Name(), err, string(body))
} Defensive patterns
Strategy: validation
Validate before calling
// verify the endpoint really returns JSON before decoding
resp, err := client.Get("https://jsnoteclub.com/ghost/api/content/posts/?limit=1")
if err == nil {
ct := resp.Header.Get("Content-Type")
if !strings.Contains(ct, "application/json") {
log.Printf("unexpected content type: %s (bot protection?)", ct)
}
} Try / catch
posts, err := p.fetchPosts(client, dataKey)
if err != nil {
if strings.Contains(err.Error(), "解析内容数据失败") {
// body wasn't JSON — likely an HTML challenge page; refresh headers/cookies or back off
}
return nil, err
} Prevention
- Check Content-Type is application/json before decoding; HTML with 200 means bot protection is active.
- Set realistic browser headers and maintain cookies to avoid challenge pages.
- Use json.Unmarshal on a buffered body so you can log a snippet of malformed responses for diagnosis.
- Pin/verify the expected schema ({"posts":[...]}) and update struct tags when the site's API changes.
When it happens
Trigger: The 200 response body from the posts API is not valid JSON: Cloudflare/bot-protection HTML page served with status 200, empty or truncated body due to timeout mid-read, gzip/encoding mismatch, or the Ghost API returning a different JSON shape that still fails to decode into {posts: []}.
Common situations: Bot protection (e.g. Cloudflare JS challenge) returns HTML with a 200 status; the site changed the response schema so fields no longer match; the 12s context cancels while streaming the body causing an unexpected EOF; a proxy intercepts and rewrites the response.
Understand the failure class
Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.
Related errors
AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07).
Data as JSON: /api/errors/7a73a3c69aa6412a.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/jsnoteclub/jsnoteclub.go:294
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) {
return entry.links
}
detailCache.Delete(detailURL)
}
ctx, cancel := context.WithTimeout(context.Background(), detailTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, detailURL, nil)
if err != nil {View on GitHub (pinned to beaa561337)