fish2018/pansou · error

[susu] 读取按钮列表失败

Error message

[susu] 读取按钮列表失败: %w

What it means

getLinks throws this when io.ReadAll fails while reading the (2 MiB-capped) button-list response body. This happens when the connection breaks mid-body, the context deadline fires during the read, or the body reader errors. The 2<<20 LimitReader means anything beyond 2 MiB is silently truncated, not errored — this error is about read failures, not size.

Solutions

  1. Unwrap the error: 'context deadline exceeded' means raise the 20s ctx timeout.
  2. Retry the request — mid-body resets are often transient.
  3. Increase doRequestWithRetry retries to cover body-read failures.
  4. Check network stability / proxy configuration between host and API.
  5. If responses legitimately exceed 2 MiB, note LimitReader truncates (it won't cause this error, but may cause [707] parse failures).

Example fix

// before
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
if err != nil {
    return nil, fmt.Errorf("[susu] 读取按钮列表失败: %w", err)
}
// after: include context cause and extend timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
if err != nil {
    return nil, fmt.Errorf("[susu] 读取按钮列表失败: %w", err) // check for context.DeadlineExceeded
}
Defensive patterns

Strategy: retry

Try / catch

links, err := p.getLinks(postID)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.ErrUnexpectedEOF) {
        // transient body read failure — retry with longer timeout
    }
}

Prevention

When it happens

Trigger: io.ReadAll(io.LimitReader(resp.Body, 2<<20)) returns err after a 200 response — connection reset mid-body, ctx (20s) deadline exceeded while streaming, or a proxy aborting the transfer.

Common situations: Very slow API responses exceeding the 20s timeout mid-download; flaky mobile/proxied networks; server closing connections under load before the body completes.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07). Data as JSON: /api/errors/1b0825382cf6fe04. Report an issue: GitHub.

Appendix: source

Thrown at plugin/susu/susu.go:364

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, ButtonListURL, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, fmt.Errorf("[susu] 创建按钮列表请求失败: %w", err)
	}
	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)

View on GitHub (pinned to beaa561337)