{"record":{"id":"a897ebe1510906dc","repo":"fish2018/pansou","slug":"susu-w-a897eb","errorCode":null,"errorMessage":"[susu] 解析按钮列表失败: %w","messagePattern":"\\[susu\\] 解析按钮列表失败: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"plugin/susu/susu.go","lineNumber":369,"sourceCode":"\tsetAPIHeaders(req, fmt.Sprintf(\"%s/%s.html\", BaseURL, postID))\n\n\tresp, err := p.doRequestWithRetry(client, req, MaxRetries)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[susu] 获取按钮列表失败: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"[susu] 按钮列表请求返回状态码: %d\", resp.StatusCode)\n\t}\n\n\trespBody, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[susu] 读取按钮列表失败: %w\", err)\n\t}\n\n\tvar groups []downloadGroup\n\tif err := json.Unmarshal(respBody, &groups); err != nil {\n\t\treturn nil, fmt.Errorf(\"[susu] 解析按钮列表失败: %w\", err)\n\t}\n\n\ttotalButtons := 0\n\tfor _, group := range groups {\n\t\ttotalButtons += len(group.Button)\n\t}\n\tif totalButtons == 0 {\n\t\treturn nil, fmt.Errorf(\"[susu] 帖子 %s 没有可用下载按钮\", postID)\n\t}\n\n\tlinkChan := make(chan model.Link, totalButtons)\n\tvar wgLinks sync.WaitGroup\n\tsemaphore := make(chan struct{}, MaxConcurrency)\n\n\tfor groupIndex, group := range groups {\n\t\tfor buttonIndex := range group.Button {\n\t\t\twgLinks.Add(1)\n\t\t\tgo func(index, i int) {","sourceCodeStart":351,"sourceCodeEnd":387,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/susu/susu.go#L351-L387","documentation":"getLinks throws this when json.Unmarshal cannot decode the button-list response body into []downloadGroup. The API returned a 200 but its body is not the expected JSON array — an HTML error/challenge page, an empty body, or a schema change (renamed/retyped fields) will all fail unmarshaling.","triggerScenarios":"json.Unmarshal(respBody, &groups) errors after reading a 200 response: body is HTML/empty/garbage, or the JSON shape no longer matches the downloadGroup struct (field name or type changed, e.g. Button array element schema drift).","commonSituations":"Site API updated its response schema; WAF returning a 200 HTML interstitial; CDN error pages served with 200; truncated responses (2 MiB LimitReader cut off valid JSON).","solutions":["Log a prefix of respBody to see whether it's JSON, HTML, or truncated.","Compare the actual JSON structure against the downloadGroup/downloadButton struct tags; update the structs if the API schema changed.","Check whether the 2 MiB LimitReader truncated the payload (unexpected end of JSON input).","Verify setAPIHeaders/cookies so the real API responds instead of a challenge page.","Use json.Unmarshal with a tolerant struct or json.Decoder with UseNumber to diagnose type mismatches."],"exampleFix":"// before\nvar groups []downloadGroup\nif err := json.Unmarshal(respBody, &groups); err != nil {\n    return nil, fmt.Errorf(\"[susu] 解析按钮列表失败: %w\", err)\n}\n// after: log the offending body on failure\nvar groups []downloadGroup\nif err := json.Unmarshal(respBody, &groups); err != nil {\n    return nil, fmt.Errorf(\"[susu] 解析按钮列表失败: %w (body prefix: %q)\", err, string(respBody[:min(200, len(respBody))]))\n}","handlingStrategy":"validation","validationCode":"if !json.Valid(respBody) {\n    // response is not JSON (challenge page / error page) — don't attempt getLinks parsing\n}","typeGuard":null,"tryCatchPattern":"links, err := p.getLinks(postID)\nif err != nil && strings.Contains(err.Error(), \"解析按钮列表失败\") {\n    // API schema likely changed; compare structs against a captured raw response\n    log.Printf(\"susu API schema drift suspected: %v\", err)\n}","preventionTips":["Add contract tests asserting the downloadGroup struct matches live API samples","Log response body prefix on unmarshal failure","Beware the 2 MiB LimitReader truncating large payloads","Refresh cookies/headers so the API responds with JSON, not challenge HTML"],"tags":["json","unmarshal","schema-change","api-response"],"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"}