fish2018/pansou · error

[ ] %sHTTP状态错误

Error message

[%s] %sHTTP状态错误: %d

What it means

The fallback branch of httpStatusError: when a response has a non-200 status and the cf-mitigated header does not indicate a challenge, the plugin returns this generic HTTP status error naming the action and code. It is the Diduan plugin's standard non-OK HTTP response error.

Solutions

  1. Log the status code and act on it: 404 means check BaseURL/SearchPath against the live site; 429 means slow down; 403 means IP blocked.
  2. Add exponential backoff for 429/5xx responses.
  3. Verify the constructed URL is correct (keyword escaping, path format).
  4. Rotate proxies or egress IPs if 403 persists.

Example fix

// before
if err != nil { return err }
// after
if err != nil {
    var httpErr *HTTPStatusError
    if errors.As(err, &httpErr) && httpErr.StatusCode == 429 {
        time.Sleep(time.Minute) // honor rate limit
    }
    return err
}
Defensive patterns

Strategy: try-catch

Try / catch

if err != nil {
    var httpErr *HTTPStatusError
    if errors.As(err, &httpErr) {
        switch httpErr.StatusCode {
        case 429:
            time.Sleep(time.Minute)
        case 404:
            return fmt.Errorf("endpoint moved; refresh URL config: %w", err)
        }
    }
    return err
}

Prevention

When it happens

Trigger: executeSearch (search page) or fetchDetailPageLinks (detail pages) receives resp.StatusCode != 200 without a Cloudflare challenge header — e.g. 404 after an endpoint/URL change, 403 from IP bans, 429 rate limiting, 5xx server errors.

Common situations: Site restructured so SearchPath/detail URLs 404; rate limiting (429) from aggressive scraping; WAF IP bans (403); temporary upstream 5xx outages.

Related errors


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

Appendix: source

Thrown at plugin/diduan/diduan.go:198

	if err != nil {
		return nil, fmt.Errorf("[%s] 解析搜索结果HTML失败: %w", p.Name(), err)
	}

	return p.parseSearchResults(doc)
}

// getPage 串行化 cloudscraper 调用,避免其 stealth 计数器并发竞争。
func (p *DiduanPlugin) getPage(rawURL string) (*http.Response, error) {
	p.scraperMu.Lock()
	defer p.scraperMu.Unlock()
	return p.scraper.Get(rawURL)
}

func (p *DiduanPlugin) httpStatusError(action string, resp *http.Response) error {
	if strings.EqualFold(resp.Header.Get("cf-mitigated"), "challenge") {
		return fmt.Errorf("[%s] %s触发 Cloudflare Managed Challenge (HTTP %d)", p.Name(), action, resp.StatusCode)
	}
	return fmt.Errorf("[%s] %sHTTP状态错误: %d", p.Name(), action, resp.StatusCode)
}

// parseSearchResults 解析搜索结果HTML
func (p *DiduanPlugin) parseSearchResults(doc *goquery.Document) ([]model.SearchResult, error) {
	var results []model.SearchResult

	// ddys.io 当前页面使用 movie-card;影视搜索区的第一个 h2 下才是搜索结果,
	// 后面的 movie-card 是推荐内容,不能一并请求详情页。
	var cards *goquery.Selection
	doc.Find("h2").EachWithBreak(func(_ int, heading *goquery.Selection) bool {
		if strings.HasPrefix(strings.TrimSpace(heading.Text()), "影视") {
			cards = heading.Parent().Parent().Find(".movie-card")
			return false
		}
		return true
	})
	if cards == nil || cards.Length() == 0 {
		// 兼容旧模板:仅扫描文章列表。

View on GitHub (pinned to beaa561337)