{"record":{"id":"200f017f04d5f39b","repo":"fish2018/pansou","slug":"s-w-200f01","errorCode":null,"errorMessage":"[%s] 解析搜索结果失败: %w","messagePattern":"\\[(.+?)\\] 解析搜索结果失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/mikuclub/mikuclub.go","lineNumber":258,"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\tsetCommonHeaders(req, \"https://www.mikuclub.uk/\")\n\n\tresp, err := p.doRequestWithRetry(req, client, searchMaxRetries)\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 postListResponse\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 *MikuclubPlugin) fetchDetailLinks(client *http.Client, postID int64, detailURL string) []model.Link {\n\tif cached, ok := detailCache.Load(postID); ok {\n\t\tif entry, valid := cached.(cacheEntry); valid {\n\t\t\tif time.Now().Before(entry.expiresAt) && len(entry.links) > 0 {\n\t\t\t\treturn entry.links\n\t\t\t}\n\t\t\tdetailCache.Delete(postID)\n\t\t}\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), detailTimeout)\n\tdefer cancel()\n","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/mikuclub/mikuclub.go#L240-L276","documentation":"This error is returned by MikuclubPlugin.fetchCategoryPosts when the HTTP body of a 200 OK response from the mikuclub.uk post_list API cannot be decoded into postListResponse via json.NewDecoder(...).Decode. It wraps the underlying json error, so type mismatches, truncated bodies, or HTML error pages served with 200 all surface here.","triggerScenarios":"fetchCategoryPosts receives StatusCode 200 but the body is not valid JSON: an anti-bot HTML interstitial, a Cloudflare/challenge page, an empty body, or a JSON schema change in postListResponse that no longer matches the struct.","commonSituations":"The site starts serving a JavaScript challenge page with status 200; the API changes its response shape (field renames/types); the response is gzipped or truncated mid-stream; proxy interference corrupts the body.","solutions":["Dump/inspect the raw response body to see whether it is HTML (protection) or malformed JSON","Check the current API response shape at the post_list endpoint and update the postListResponse struct tags","If the body is a bot-challenge page, add gate handling/headers like the miosou plugin does","Retry later if the body was truncated due to a transient upstream failure"],"exampleFix":"// before\nvar payload postListResponse\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))\nif looksLikeHTML(body) {\n    return nil, fmt.Errorf(\"[%s] 收到防爬页面而非 JSON\", p.Name())\n}\nvar payload postListResponse\nif err := json.Unmarshal(body, &payload); err != nil {\n    return nil, fmt.Errorf(\"[%s] 解析搜索结果失败: %w (body: %.200s)\", p.Name(), err, body)\n}","handlingStrategy":"validation","validationCode":"// Go: sniff body before decoding\nbody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))\nif len(bytes.TrimSpace(body)) == 0 || bytes.Contains(bytes.ToLower(body[:min(200,len(body))]), []byte(\"<html\")) {\n    // protection page or empty: do not attempt JSON decode\n}","typeGuard":"func looksLikeJSON(b []byte) bool {\n    t := bytes.TrimSpace(b)\n    return len(t) > 0 && (t[0] == '{' || t[0] == '[')\n}","tryCatchPattern":"if err := json.Unmarshal(body, &payload); err != nil {\n    var jsonErr *json.SyntaxError\n    if errors.As(err, &jsonErr) {\n        log.Printf(\"bad JSON at offset %d: %v; body head: %.200s\", jsonErr.Offset, jsonErr, body)\n    }\n    return nil, err\n}","preventionTips":["Always cap body reads with io.LimitReader","Log a body snippet on decode failure for diagnosis","Re-verify struct tags after upstream site updates","Watch for 200-with-HTML anti-bot pages"],"tags":["json","decode","upstream","unexpected-response"],"backgroundTag":"json-decode-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"}