fish2018/pansou · error

[ ] API JSON 解析失败

Error message

[%s] API JSON 解析失败: %w

What it means

searchImpl unmarshals the API body into FeikuaiAPIResponse; if the payload is not valid JSON or does not match the struct, it falls back to searchWeb with this wrapped error. It means the endpoint answered but not with the expected JSON schema.

Solutions

  1. Log a snippet of body on failure to see what was actually returned
  2. Check Content-Type; handle non-JSON (HTML challenge) explicitly
  3. Verify FeikuaiAPIResponse fields still match the current API schema
  4. If Content-Encoding is gzip and not auto-decompressed, wrap with gzip.NewReader

Example fix

// before
var apiResp FeikuaiAPIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
    return p.searchWeb(client, keyword, fmt.Errorf("[%s] API JSON 解析失败: %w", p.Name(), err))
}
// after
var apiResp FeikuaiAPIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
    log.Printf("feikuai non-JSON response (first 200B): %.200s", body)
    return p.searchWeb(client, keyword, fmt.Errorf("[%s] API JSON 解析失败: %w", p.Name(), err))
}
Defensive patterns

Strategy: validation

Validate before calling

ct := resp.Header.Get("Content-Type")
if !strings.Contains(ct, "application/json") {
    return fmt.Errorf("expected JSON, got %q", ct)
}
if len(body) == 0 { return fmt.Errorf("empty body") }

Type guard

func looksLikeFeikuaiAPI(b []byte) bool {
    var probe struct { Code int `json:"code"` }
    return json.Unmarshal(b, &probe) == nil
}

Try / catch

results, err := plugin.Search(keyword)
if err != nil && strings.Contains(err.Error(), "JSON 解析失败") {
    log.Printf("feikuai returned non-JSON (likely challenge page); using fallback results: %v", err)
}

Prevention

When it happens

Trigger: json.Unmarshal(body, &apiResp) fails — HTML error page instead of JSON, empty body, changed field types (e.g. code became string), gzip content not auto-decompressed.

Common situations: Site replaced API with an HTML challenge page (anti-bot), API version change altering the response shape, proxy injecting a captive-portal page.

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/ad8669b3366e04e0. Report an issue: GitHub.

Appendix: source

Thrown at plugin/feikuai/feikuai.go:144

	if err != nil {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] 搜索 API 请求失败: %w", p.Name(), err))
	}
	defer resp.Body.Close()

	// 检查状态码
	if resp.StatusCode != 200 {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] 搜索 API 返回状态码: %d", p.Name(), resp.StatusCode))
	}

	// 读取并解析JSON响应
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] 读取 API 响应失败: %w", p.Name(), err))
	}

	var apiResp FeikuaiAPIResponse
	if err := json.Unmarshal(body, &apiResp); err != nil {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] API JSON 解析失败: %w", p.Name(), err))
	}

	// 检查API响应状态
	if apiResp.Code != 0 {
		return p.searchWeb(client, keyword, fmt.Errorf("[%s] API 返回错误: %s (code: %d)", p.Name(), apiResp.Msg, apiResp.Code))
	}

	// 解析搜索结果
	var results []model.SearchResult
	for _, item := range apiResp.Items {
		// 每个item可能包含多个种子
		for _, torrent := range item.Torrents {
			result := p.parseTorrent(keyword, item, torrent)
			if result.Title != "" && len(result.Links) > 0 {
				results = append(results, result)
			}
		}
	}

View on GitHub (pinned to beaa561337)