{"record":{"id":"82207dff72e10bb1","repo":"fish2018/pansou","slug":"s-d-82207d","errorCode":null,"errorMessage":"[%s] 内容接口返回状态码: %d","messagePattern":"\\[(.+?)\\] 内容接口返回状态码: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/jsnoteclub/jsnoteclub.go","lineNumber":289,"sourceCode":"\treqURL := fmt.Sprintf(\"https://jsnoteclub.com/ghost/api/content/posts/?%s\", params.Encode())\n\n\tctx, cancel := context.WithTimeout(context.Background(), requestTimeout)\n\tdefer cancel()\n\n\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","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/jsnoteclub/jsnoteclub.go#L271-L307","documentation":"fetchPosts returns this error when the Ghost Content API responds with a status code other than 200 after all retries. The plugin treats only HTTP 200 as success and reports the actual code in the message. This typically means the upstream jsnoteclub.com service rejected the request (auth on the data key, rate limiting, blocks) or is having server-side problems.","triggerScenarios":"resp.StatusCode != http.StatusOK (e.g. 401/403 for a bad or expired key, 404 if the API path changed, 429 rate limit, 5xx server errors) on the GET to https://jsnoteclub.com/ghost/api/content/posts/ with the data-key query parameter.","commonSituations":"The site enabled Cloudflare/bot protection returning 403 challenge pages; the scraped data-key is stale or invalid; the Ghost API version or path changed; the server returns 502/503 during downtime; the request is rate-limited due to the limit=10000 query.","solutions":["Log/capture the status code from the message and handle the specific code: 401/403 → re-fetch a fresh data-key via fetchDataKey; 429 → slow down; 5xx → retry later.","Re-run the search after the 1-hour posts cache expires or restart the process to force re-scraping the homepage key.","Update headers (User-Agent/Referer) in setAPIHeaders if the site's bot protection is rejecting requests.","Verify the API endpoint URL is still current (Ghost Content API path may have changed); update the hard-coded base URL if the site migrated.","Reduce limit=10000 or other aggressive query parameters if the server rejects oversized requests."],"exampleFix":"// before\nif resp.StatusCode != http.StatusOK {\n    return nil, fmt.Errorf(\"[%s] 内容接口返回状态码: %d\", p.Name(), resp.StatusCode)\n}\n// after\nif resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {\n    return nil, fmt.Errorf(\"[%s] key invalid/blocked (status %d), refresh data-key\", p.Name(), resp.StatusCode)\n}\nif resp.StatusCode != http.StatusOK {\n    return nil, fmt.Errorf(\"[%s] 内容接口返回状态码: %d\", p.Name(), resp.StatusCode)\n}","handlingStrategy":"retry","validationCode":"// probe the API endpoint and check the status before full usage\nreq, _ := http.NewRequest(\"GET\", \"https://jsnoteclub.com/ghost/api/content/posts/?limit=1\", nil)\nresp, err := client.Do(req)\nif err == nil && resp.StatusCode != http.StatusOK {\n    log.Printf(\"posts API unhealthy: %d\", resp.StatusCode)\n}","typeGuard":null,"tryCatchPattern":"posts, err := p.fetchPosts(client, dataKey)\nif err != nil {\n    var statusErr *fmt.Errorf\n    if strings.Contains(err.Error(), \"内容接口返回状态码\") {\n        // parse the code and branch: 403/429 → backoff & refresh key; 5xx → retry later\n    }\n    return nil, err\n}","preventionTips":["Re-scrape the homepage data-key when receiving 401/403; keys may rotate.","Respect rate limits: throttle parallel detail fetches and keep caching enabled.","Keep the User-Agent/Referer headers realistic to pass bot protection.","Alert on persistent 5xx so you know it is a site outage, not a config problem."],"tags":["http","status-code","api","upstream"],"backgroundTag":"http-non-200-response","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"}