fish2018/pansou · error
[ ] parse json failed on page
Error message
[%s] parse json failed on page %d: %w
What it means
After reading a Discourse search page, searchImpl unmarshals the body into SearchResponse. If json.Unmarshal fails on the first page (no accumulated results), it returns this error wrapping the JSON error with the page number; otherwise it warns and stops.
Solutions
- Inspect the raw response body to confirm whether it is HTML (bot challenge) instead of JSON
- Ensure cloudscraper is properly initialized so anti-bot challenges are solved before the request
- Verify the site's search API endpoint still returns the expected JSON shape
- Log body[:200] on parse failure to diagnose the actual content
Example fix
// before
if err := json.Unmarshal(body, &searchResp); err != nil {
return nil, fmt.Errorf("[%s] parse json failed on page %d: %w", p.Name(), currentPage, err)
}
// after
if err := json.Unmarshal(body, &searchResp); err != nil {
return nil, fmt.Errorf("[%s] parse json failed on page %d (body head: %q): %w", p.Name(), currentPage, body[:min(200, len(body))], err)
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-flight: confirm the search endpoint returns JSON
resp, _ := http.Get(baseURL + "/search.json?q=test")
ct, _ := io.ReadAll(io.LimitReader(resp.Body, 64))
if !strings.HasPrefix(strings.TrimSpace(string(ct)), "{") {
return errors.New("endpoint not returning JSON; anti-bot likely")
} Try / catch
links, err := plugin.Search(query)
if err != nil && strings.Contains(err.Error(), "parse json failed") {
log.Printf("non-JSON response from discourse (bot challenge?); refreshing scraper session")
return nil, ErrBotChallenge
} Prevention
- Keep cloudscraper initialized and cookies fresh
- Check for 200-with-HTML responses when sites update their anti-bot
- Pin/verify Discourse API schema when upgrading a forum
When it happens
Trigger: A Discourse search page body is not valid JSON matching SearchResponse — e.g. an HTML Cloudflare challenge/login page or an error page was returned with status 200.
Common situations: Cloudflare or anti-bot interstitial served with 200 status, forum returning HTML error pages, API response shape changed across Discourse versions.
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/533123f6598bc272.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/discourse/discourse.go:253
// 读取响应体
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
if len(allResults) > 0 {
fmt.Printf("[%s] Warning: failed to read page %d: %v\n", p.Name(), currentPage, err)
break
}
return nil, fmt.Errorf("[%s] read response failed on page %d: %w", p.Name(), currentPage, err)
}
// 解析JSON响应
var searchResp SearchResponse
if err := json.Unmarshal(body, &searchResp); err != nil {
if len(allResults) > 0 {
fmt.Printf("[%s] Warning: failed to parse page %d: %v\n", p.Name(), currentPage, err)
break
}
return nil, fmt.Errorf("[%s] parse json failed on page %d: %w", p.Name(), currentPage, err)
}
// 如果没有帖子了,停止获取
if len(searchResp.Posts) == 0 {
break
}
// 转换为SearchResult并去重
pageResults := p.convertToSearchResults(searchResp)
// 添加结果(去重)
for _, result := range pageResults {
// 从 UniqueID 中提取帖子ID
var postID int
fmt.Sscanf(result.UniqueID, "discourse-%d", &postID)
if !seenPostIDs[postID] {
seenPostIDs[postID] = trueView on GitHub (pinned to beaa561337)