fish2018/pansou · error

[ ] 解析搜索结果失败

Error message

[%s] 解析搜索结果失败: %w

What it means

This error is returned by MikuclubPlugin.fetchCategoryPosts when the HTTP body of a 200 OK response from the mikuclub.uk post_list API cannot be decoded into postListResponse via json.NewDecoder(...).Decode. It wraps the underlying json error, so type mismatches, truncated bodies, or HTML error pages served with 200 all surface here.

Solutions

  1. Dump/inspect the raw response body to see whether it is HTML (protection) or malformed JSON
  2. Check the current API response shape at the post_list endpoint and update the postListResponse struct tags
  3. If the body is a bot-challenge page, add gate handling/headers like the miosou plugin does
  4. Retry later if the body was truncated due to a transient upstream failure

Example fix

// before
var payload postListResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
    return nil, fmt.Errorf("[%s] 解析搜索结果失败: %w", p.Name(), err)
}
// after
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if looksLikeHTML(body) {
    return nil, fmt.Errorf("[%s] 收到防爬页面而非 JSON", p.Name())
}
var payload postListResponse
if err := json.Unmarshal(body, &payload); err != nil {
    return nil, fmt.Errorf("[%s] 解析搜索结果失败: %w (body: %.200s)", p.Name(), err, body)
}
Defensive patterns

Strategy: validation

Validate before calling

// Go: sniff body before decoding
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if len(bytes.TrimSpace(body)) == 0 || bytes.Contains(bytes.ToLower(body[:min(200,len(body))]), []byte("<html")) {
    // protection page or empty: do not attempt JSON decode
}

Type guard

func looksLikeJSON(b []byte) bool {
    t := bytes.TrimSpace(b)
    return len(t) > 0 && (t[0] == '{' || t[0] == '[')
}

Try / catch

if err := json.Unmarshal(body, &payload); err != nil {
    var jsonErr *json.SyntaxError
    if errors.As(err, &jsonErr) {
        log.Printf("bad JSON at offset %d: %v; body head: %.200s", jsonErr.Offset, jsonErr, body)
    }
    return nil, err
}

Prevention

When it happens

Trigger: fetchCategoryPosts receives StatusCode 200 but the body is not valid JSON: an anti-bot HTML interstitial, a Cloudflare/challenge page, an empty body, or a JSON schema change in postListResponse that no longer matches the struct.

Common situations: The site starts serving a JavaScript challenge page with status 200; the API changes its response shape (field renames/types); the response is gzipped or truncated mid-stream; proxy interference corrupts the body.

Related errors


AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07). Data as JSON: /api/errors/200f017f04d5f39b. Report an issue: GitHub.

Appendix: source

Thrown at plugin/mikuclub/mikuclub.go:258

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
	if err != nil {
		return nil, fmt.Errorf("[%s] 创建搜索请求失败: %w", p.Name(), err)
	}
	setCommonHeaders(req, "https://www.mikuclub.uk/")

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

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("[%s] 搜索返回状态码: %d", p.Name(), resp.StatusCode)
	}

	var payload postListResponse
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return nil, fmt.Errorf("[%s] 解析搜索结果失败: %w", p.Name(), err)
	}

	return payload.Posts, nil
}

func (p *MikuclubPlugin) fetchDetailLinks(client *http.Client, postID int64, detailURL string) []model.Link {
	if cached, ok := detailCache.Load(postID); ok {
		if entry, valid := cached.(cacheEntry); valid {
			if time.Now().Before(entry.expiresAt) && len(entry.links) > 0 {
				return entry.links
			}
			detailCache.Delete(postID)
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), detailTimeout)
	defer cancel()

View on GitHub (pinned to beaa561337)