fish2018/pansou · error
解析响应失败
Error message
解析响应失败: %w
What it means
fetchFirstPage wraps json.Unmarshal failure when the pansearch.me response body cannot be decoded into PanSearchResponse. Because the target is a Next.js /_next/data endpoint, the body is usually JSON — so this error means the body was empty, HTML (an error/interstitial page), or the site's response shape changed so PanSearchResponse no longer matches.
Solutions
- Log a snippet of respBody on unmarshal failure to see what was actually returned
- Verify the body starts with '{' before unmarshalling; treat HTML/empty bodies as upstream errors
- Re-check the current JSON structure on the live site and update the PanSearchResponse struct tags (json:"...")
- Ensure Content-Encoding is handled (set Accept-Encoding: gzip and decompress, or disable compression)
Example fix
// before
var apiResp PanSearchResponse
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, 0, fmt.Errorf("解析响应失败: %w", err)
}
// after
if len(respBody) == 0 || respBody[0] != '{' {
return nil, 0, fmt.Errorf("解析响应失败: 非JSON响应(可能是反爬页面): %.200s", respBody)
}
var apiResp PanSearchResponse
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, 0, fmt.Errorf("解析响应失败: %w, body: %.200s", err, respBody)
} Defensive patterns
Strategy: validation
Validate before calling
if len(respBody) == 0 {
return fmt.Errorf("empty response body")
}
if !json.Valid(respBody) {
return fmt.Errorf("non-JSON response (anti-bot page?): %.200s", respBody)
} Type guard
func isJSONBody(b []byte) bool {
t := bytes.TrimSpace(b)
return len(t) > 0 && (t[0] == '{' || t[0] == '[')
} Try / catch
items, total, err := p.fetchFirstPage(ctx, query)
if err != nil && strings.Contains(err.Error(), "解析响应失败") {
log.Printf("pansearch returned unparseable body; check anti-bot/schema: %v", err)
return ErrUpstreamChanged
} Prevention
- Always validate the body is JSON before unmarshalling
- Log body snippets on parse failure to detect anti-bot HTML pages
- Re-verify struct json tags after any site redeploy
- Handle Content-Encoding (gzip) explicitly
When it happens
Trigger: fetchFirstPage (called by doSearch) gets a 200 response but json.Unmarshal(respBody, &apiResp) fails: body is HTML from a challenge page, gzip/encoding mismatch, empty body, or new JSON schema after a site redeploy.
Common situations: Cloudflare/anti-bot interstitial served with 200 status; site redeploy renamed pageProps.data fields; response compressed but not decompressed; truncated body from an earlier partial read.
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/0da9d03b7b29ba3c.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/pansearch/pansearch.go:627
// 检查状态码
if resp.StatusCode == 404 {
return nil, 0, fmt.Errorf("404 Not Found,buildId可能已过期")
}
if resp.StatusCode != 200 {
return nil, 0, fmt.Errorf("服务器返回非200状态码: %d", resp.StatusCode)
}
// 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, fmt.Errorf("读取响应失败: %w", err)
}
// 解析响应
var apiResp PanSearchResponse
if err := json.Unmarshal(respBody, &apiResp); err != nil {
return nil, 0, fmt.Errorf("解析响应失败: %w", err)
}
// 获取total和结果
total := apiResp.PageProps.Data.Total
items := apiResp.PageProps.Data.Data
return items, total, nil
}
// fetchPage 获取指定偏移量的页面
func (p *PanSearchAsyncPlugin) fetchPage(keyword string, offset int, baseURL string, client *http.Client) ([]PanSearchItem, error) {
// 构建请求URL
reqURL := fmt.Sprintf("%s?keyword=%s&offset=%d", baseURL, url.QueryEscape(keyword), offset)
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
defer cancel()
View on GitHub (pinned to beaa561337)