fish2018/pansou · error

解析 Seroval 响应失败

Error message

解析 Seroval 响应失败: %w

What it means

parseSearchResponse unmarshals the search API response as a Seroval (server-function serialization) node using encoding/json. If stdjson.Unmarshal fails, the raw body was not valid JSON in the expected Seroval envelope, and the underlying parse error is wrapped with this message.

Solutions

  1. Log the raw response body and status code on failure to see what was actually returned (HTML error page vs JSON).
  2. Re-run discovery (refreshServerFunctionID) to obtain a fresh server function ID — a stale ID often yields error responses.
  3. Check for bot-blocking (captcha/HTML challenge) and add appropriate headers, cookies, or slower request pacing.
  4. Update serovalNode struct fields if the Seroval envelope format changed upstream.
Defensive patterns

Strategy: try-catch

Validate before calling

if !stdjson.Valid(body) {
	return fmt.Errorf("response is not JSON (first 200 bytes: %.200q)", body)
}

Type guard

func isJSONResponse(body []byte) bool { return stdjson.Valid(body) }

Try / catch

results, err := parseSearchResponse(body)
if err != nil {
	var perr *stdjson.UnmarshalTypeError
	if errors.As(err, &perr) {
		log.Printf("seroval schema drift at %v", perr)
	}
	log.Printf("raw body: %.500q", body)
	return nil, err
}

Prevention

When it happens

Trigger: The search endpoint returned HTML (login/captcha/error page), an empty body, truncated output, or a JSON shape incompatible with the serovalNode struct, so stdjson.Unmarshal(body, &root) errors.

Common situations: The server function ID is stale/invalid so the API returns an error page; rate limiting or WAF returns HTML; the site switched serialization format after a framework upgrade; response truncated by a size limit.

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

Appendix: source

Thrown at plugin/xiaokupan/xiaokupan.go:266

		return "", fmt.Errorf("读取入口脚本失败: %w", err)
	}

	routeIndex := strings.Index(string(assetBody), "/s/$query")
	if routeIndex < 0 {
		return "", fmt.Errorf("入口脚本未找到搜索路由")
	}
	windowStart := max(0, routeIndex-2048)
	hashes := hashPattern.FindAll(assetBody[windowStart:routeIndex], -1)
	if len(hashes) == 0 {
		return "", fmt.Errorf("入口脚本未找到搜索接口标识")
	}
	return string(hashes[len(hashes)-1]), nil
}

func parseSearchResponse(body []byte) ([]model.SearchResult, error) {
	var root serovalNode
	if err := stdjson.Unmarshal(body, &root); err != nil {
		return nil, fmt.Errorf("解析 Seroval 响应失败: %w", err)
	}
	decoder := newSerovalDecoder(&root)
	resultNode := decoder.objectValue(&root, "result")
	searchResultsNode := decoder.objectValue(resultNode, "searchResults")
	mergedNode := decoder.objectValue(searchResultsNode, "merged_by_type")
	mergedNode = decoder.resolve(mergedNode)
	if mergedNode == nil || mergedNode.Props == nil {
		return nil, fmt.Errorf("响应缺少 merged_by_type")
	}

	results := make([]model.SearchResult, 0)
	seenURLs := make(map[string]struct{})
	for index, linkType := range mergedNode.Props.Keys {
		if index >= len(mergedNode.Props.Values) {
			break
		}
		arrayNode := decoder.resolve(mergedNode.Props.Values[index])
		if arrayNode == nil || arrayNode.Type != 9 {

View on GitHub (pinned to beaa561337)