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

  1. Log a prefix of respBody to see whether it's JSON, HTML, or truncated.
  2. Compare the actual JSON structure against the downloadGroup/downloadButton struct tags; update the structs if the API schema changed.
  3. Check whether the 2 MiB LimitReader truncated the payload (unexpected end of JSON input).
  4. Verify setAPIHeaders/cookies so the real API responds instead of a challenge page.
  5. 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

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)