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
- 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.
- Add exponential backoff for 429/5xx responses.
- Verify the constructed URL is correct (keyword escaping, path format).
- 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
- Handle 429 with backoff and 404 with URL-config refresh explicitly.
- Monitor status-code distribution per target site.
- Rotate proxies when persistent 403s appear.
- Validate constructed URLs against the live site when endpoints change.
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)