fish2018/pansou · error
[susu] 解析按钮列表失败
Error message
[susu] 解析按钮列表失败: %w
What it means
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.
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.
Example fix
// before
var groups []downloadGroup
if err := json.Unmarshal(respBody, &groups); err != nil {
return nil, fmt.Errorf("[susu] 解析按钮列表失败: %w", err)
}
// after: log the offending body on failure
var groups []downloadGroup
if err := json.Unmarshal(respBody, &groups); err != nil {
return nil, fmt.Errorf("[susu] 解析按钮列表失败: %w (body prefix: %q)", err, string(respBody[:min(200, len(respBody))]))
} Defensive patterns
Strategy: validation
Validate before calling
if !json.Valid(respBody) {
// response is not JSON (challenge page / error page) — don't attempt getLinks parsing
} Try / catch
links, err := p.getLinks(postID)
if err != nil && strings.Contains(err.Error(), "解析按钮列表失败") {
// API schema likely changed; compare structs against a captured raw response
log.Printf("susu API schema drift suspected: %v", err)
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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).
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/a897ebe1510906dc.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/susu/susu.go:369
setAPIHeaders(req, fmt.Sprintf("%s/%s.html", BaseURL, postID))
resp, err := p.doRequestWithRetry(client, req, MaxRetries)
if err != nil {
return nil, fmt.Errorf("[susu] 获取按钮列表失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("[susu] 按钮列表请求返回状态码: %d", resp.StatusCode)
}
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
if err != nil {
return nil, fmt.Errorf("[susu] 读取按钮列表失败: %w", err)
}
var groups []downloadGroup
if err := json.Unmarshal(respBody, &groups); err != nil {
return nil, fmt.Errorf("[susu] 解析按钮列表失败: %w", err)
}
totalButtons := 0
for _, group := range groups {
totalButtons += len(group.Button)
}
if totalButtons == 0 {
return nil, fmt.Errorf("[susu] 帖子 %s 没有可用下载按钮", postID)
}
linkChan := make(chan model.Link, totalButtons)
var wgLinks sync.WaitGroup
semaphore := make(chan struct{}, MaxConcurrency)
for groupIndex, group := range groups {
for buttonIndex := range group.Button {
wgLinks.Add(1)
go func(index, i int) {View on GitHub (pinned to beaa561337)