fish2018/pansou · error
[ ] 解析 API 响应失败
Error message
[%s] 解析 API 响应失败: %w
What it means
dyyj.executeSearchAPI received a response but the JSON decoder could not unmarshal it into dyyjAPIResponse (encodingjson.NewDecoder(...).Decode(&payload) failed). This means the body was not valid JSON or its shape broke decoding (e.g. HTML error page returned with 200, truncated JSON).
Solutions
- Log the first 200 bytes of the raw body before decoding to see what arrived
- Check resp.StatusCode and Content-Type; fail early if Content-Type is not JSON
- Verify the Flarum API endpoint still exists and matches the dyyjAPIResponse struct
- Handle authentication requirements (Flarum may need a token for some queries)
- Retry on transient truncation
Example fix
// before
var payload dyyjAPIResponse
if err := encodingjson.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, fmt.Errorf("[%s] 解析 API 响应失败: %w", p.Name(), err)
}
// after
ct := resp.Header.Get("Content-Type")
if !strings.Contains(ct, "json") {
header, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
return nil, fmt.Errorf("[%s] API 返回非 JSON 响应 (Content-Type=%s): %s", p.Name(), ct, header)
}
var payload dyyjAPIResponse
if err := encodingjson.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, fmt.Errorf("[%s] 解析 API 响应失败: %w", p.Name(), err)
} Defensive patterns
Strategy: type-guard
Validate before calling
if !strings.Contains(resp.Header.Get("Content-Type"), "json") {
return errors.New("API did not return JSON")
} Type guard
func isJSONResponse(resp *http.Response) bool {
return resp.StatusCode == http.StatusOK && strings.Contains(resp.Header.Get("Content-Type"), "json")
} Try / catch
var payload dyyjAPIResponse
if err := encodingjson.NewDecoder(io.LimitReader(resp.Body, maxBodySize)).Decode(&payload); err != nil {
header, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
return nil, fmt.Errorf("decode API response: %w (head=%q)", err, header)
} Prevention
- Check Content-Type and status before decoding
- Capture a raw body prefix on decode failure for diagnosis
- Keep the response struct in sync with the Flarum API version
- Watch for HTML challenge/login pages arriving with 200 status
When it happens
Trigger: Flarum API returned non-JSON content (HTML login/challenge page, empty body, or a plain-text error) while the plugin expected application/vnd.api+json; response truncated mid-stream.
Common situations: Session expired so the API returns an HTML redirect/login page; WAF interposes an HTML challenge; API version change altering the response envelope; server 500 with text body.
Related errors
AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07).
Data as JSON: /api/errors/9c79c22259fb34c2.
Report an issue: GitHub.
Appendix: source
Thrown at plugin/dyyj/dyyj.go:429
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
if err != nil {
return nil, fmt.Errorf("[%s] 创建 API 请求失败: %w", p.Name(), err)
}
req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Accept", "application/vnd.api+json, application/json")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Referer", BaseURL+"/")
resp, err := p.doRequestWithRetry(req, client)
if err != nil {
return nil, fmt.Errorf("[%s] API 搜索请求失败: %w", p.Name(), err)
}
defer resp.Body.Close()
var payload dyyjAPIResponse
if err := encodingjson.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, fmt.Errorf("[%s] 解析 API 响应失败: %w", p.Name(), err)
}
posts := make(map[string]dyyjIncluded, len(payload.Included))
for _, post := range payload.Included {
posts[post.ID] = post
}
results := make([]model.SearchResult, 0, len(payload.Data))
for _, discussion := range payload.Data {
post, ok := posts[discussion.Relationships.MostRelevantPost.Data.ID]
if !ok {
continue
}
links := p.extractAPIContentLinks(post.Attributes.ContentHTML)
if len(links) == 0 {
continue
}
createdAt, parseErr := time.Parse(time.RFC3339, discussion.Attributes.CreatedAt)View on GitHub (pinned to beaa561337)