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

  1. Log a snippet of respBody on unmarshal failure to see what was actually returned
  2. Verify the body starts with '{' before unmarshalling; treat HTML/empty bodies as upstream errors
  3. Re-check the current JSON structure on the live site and update the PanSearchResponse struct tags (json:"...")
  4. 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

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)