fish2018/pansou · error

[ ] 解析详情响应失败

Error message

[%s] 解析详情响应失败: %w

What it means

The getVideoDetail response body failed to json.Unmarshal into lingjiDetailResponse — the body was not JSON matching the expected envelope. Analogous to the search parse failure but on the detail path.

Solutions

  1. Log a prefix of the raw body on failure to identify what came back
  2. Update the lingjiDetailResponse struct to match the current detail schema
  3. Check content-type before unmarshaling and convert HTML responses into a distinct upstream error
  4. Retry on transient corruption
  5. Verify the id parameter is valid — some APIs return non-JSON for unknown ids

Example fix

// before
var resp lingjiDetailResponse
if err := json.Unmarshal(body, &resp); err != nil {
    return lingjiVideoItem{}, fmt.Errorf("[%s] 解析详情响应失败: %w", p.Name(), err)
}
// after
if len(body) == 0 {
    return lingjiVideoItem{}, fmt.Errorf("[%s] 详情响应为空", p.Name())
}
var resp lingjiDetailResponse
if err := json.Unmarshal(body, &resp); err != nil {
    return lingjiVideoItem{}, fmt.Errorf("[%s] 解析详情响应失败: %w (body=%q)", p.Name(), err, body[:min(len(body),200)])
}
Defensive patterns

Strategy: validation

Validate before calling

// Require non-empty JSON object body before typed unmarshal
if len(body) == 0 || body[0] != '{' {
    return lingjiVideoItem{}, fmt.Errorf("详情响应非JSON: %q", body[:min(len(body),100)])
}

Type guard

func looksLikeLingjiDetail(body []byte) bool {
    var probe struct {
        Success *bool `json:"success"`
        Code    *int  `json:"code"`
        Data    json.RawMessage `json:"data"`
    }
    return json.Unmarshal(body, &probe) == nil && probe.Success != nil && probe.Data != nil
}

Try / catch

item, err := fetchDetail(doubID)
if err != nil && strings.Contains(err.Error(), "解析详情响应失败") {
    log.Printf("lingjisp detail schema drift: %v", err)
    return emptyItem, nil // skip this title, keep scraping
}

Prevention

When it happens

Trigger: doLingjiGET succeeded but returned HTML (error page), an empty body, or JSON whose shape differs from lingjiDetailResponse (e.g. data is a string instead of object), breaking unmarshal.

Common situations: CDN/WAF returned an HTML block page; API schema changed for detail responses; truncated or corrupted body on flaky networks; parked domain serving ads HTML.

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

Appendix: source

Thrown at plugin/lingjisp/lingjisp.go:226

	}
	return dedupeLingjiItems(items), nil
}

func (p *LingjiPlugin) fetchDetail(client *http.Client, doubID int) (lingjiVideoItem, error) {
	params := url.Values{}
	params.Set("app_id", lingjiAppID)
	params.Set("identity", lingjiIdentity)
	params.Set("id", fmt.Sprintf("%d", doubID))

	apiURL := lingjiAPIBase + "getVideoDetail?" + params.Encode()
	body, err := doLingjiGET(client, apiURL, lingjiDetailTimeout)
	if err != nil {
		return lingjiVideoItem{}, fmt.Errorf("[%s] 详情请求失败: %w", p.Name(), err)
	}

	var resp lingjiDetailResponse
	if err := json.Unmarshal(body, &resp); err != nil {
		return lingjiVideoItem{}, fmt.Errorf("[%s] 解析详情响应失败: %w", p.Name(), err)
	}
	if !resp.Success || resp.Code != http.StatusOK {
		return lingjiVideoItem{}, fmt.Errorf("[%s] 详情接口返回异常: success=%v code=%d", p.Name(), resp.Success, resp.Code)
	}
	return resp.Data, nil
}

func doLingjiGET(client *http.Client, requestURL string, timeout time.Duration) ([]byte, error) {
	var lastErr error

	for attempt := 0; attempt < lingjiMaxRetries; attempt++ {
		ctx, cancel := context.WithTimeout(context.Background(), timeout)
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
		if err != nil {
			cancel()
			return nil, err
		}

View on GitHub (pinned to beaa561337)