{"record":{"id":"7a73a3c69aa6412a","repo":"fish2018/pansou","slug":"s-w-7a73a3","errorCode":null,"errorMessage":"[%s] 解析内容数据失败: %w","messagePattern":"\\[(.+?)\\] 解析内容数据失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/jsnoteclub/jsnoteclub.go","lineNumber":294,"sourceCode":"\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[%s] 创建内容请求失败: %w\", p.Name(), err)\n\t}\n\tsetAPIHeaders(req, \"https://jsnoteclub.com/\")\n\n\tresp, err := p.doRequestWithRetry(req, client, maxRequestRetries)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[%s] 获取内容失败: %w\", p.Name(), err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"[%s] 内容接口返回状态码: %d\", p.Name(), resp.StatusCode)\n\t}\n\n\tvar payload ghostPostsResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {\n\t\treturn nil, fmt.Errorf(\"[%s] 解析内容数据失败: %w\", p.Name(), err)\n\t}\n\n\treturn payload.Posts, nil\n}\n\nfunc (p *JsNoteClubPlugin) fetchDetailLinks(client *http.Client, detailURL string) []model.Link {\n\tif cached, ok := detailCache.Load(detailURL); ok {\n\t\tif entry, valid := cached.(detailCacheEntry); valid && time.Now().Before(entry.expiresAt) {\n\t\t\treturn entry.links\n\t\t}\n\t\tdetailCache.Delete(detailURL)\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), detailTimeout)\n\tdefer cancel()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, detailURL, nil)\n\tif err != nil {","sourceCodeStart":276,"sourceCodeEnd":312,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/jsnoteclub/jsnoteclub.go#L276-L312","documentation":"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.","triggerScenarios":"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: []}.","commonSituations":"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.","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."],"exampleFix":"// before\nvar payload ghostPostsResponse\nif err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {\n    return nil, fmt.Errorf(\"[%s] 解析内容数据失败: %w\", p.Name(), err)\n}\n// after\nbody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))\nvar payload ghostPostsResponse\nif err := json.Unmarshal(body, &payload); err != nil {\n    return nil, fmt.Errorf(\"[%s] 解析内容数据失败: %w (body starts with: %.100s)\", p.Name(), err, string(body))\n}","handlingStrategy":"validation","validationCode":"// verify the endpoint really returns JSON before decoding\nresp, err := client.Get(\"https://jsnoteclub.com/ghost/api/content/posts/?limit=1\")\nif err == nil {\n    ct := resp.Header.Get(\"Content-Type\")\n    if !strings.Contains(ct, \"application/json\") {\n        log.Printf(\"unexpected content type: %s (bot protection?)\", ct)\n    }\n}","typeGuard":null,"tryCatchPattern":"posts, err := p.fetchPosts(client, dataKey)\nif err != nil {\n    if strings.Contains(err.Error(), \"解析内容数据失败\") {\n        // body wasn't JSON — likely an HTML challenge page; refresh headers/cookies or back off\n    }\n    return nil, err\n}","preventionTips":["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."],"tags":["json","decoding","api","response-parsing"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"beaa56133755a548ebc51b090b3816e2ae044aa6","analyzedAt":"2026-09-07T00:31:18.025Z","contentChangedAt":"2026-09-07T00:31:18.025Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}