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
- Unwrap the error: 'context deadline exceeded' means raise the 20s ctx timeout.
- Retry the request — mid-body resets are often transient.
- Increase doRequestWithRetry retries to cover body-read failures.
- Check network stability / proxy configuration between host and API.
- 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
- Allow enough context timeout for slow bodies
- Retry mid-body read failures
- Avoid unstable proxies between host and API
- Watch for repeated ErrUnexpectedEOF indicating server-side connection drops
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)