{"record":{"id":"84e3e14476223404","repo":"fish2018/pansou","slug":"rate-limited","errorCode":null,"errorMessage":"rate limited","messagePattern":"rate limited","errorType":"exception","errorClass":null,"httpStatus":429,"severity":"warning","filePath":"plugin/yuhuage/yuhuage.go","lineNumber":72,"sourceCode":"}\n\n// SearchWithResult 执行搜索并返回包含IsFinal标记的结果\nfunc (p *YuhuagePlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {\n\treturn p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)\n}\n\n// searchImpl 搜索实现方法\nfunc (p *YuhuagePlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {\n\tif p.debugMode {\n\t\tlog.Printf(\"[YUHUAGE] 开始搜索: %s\", keyword)\n\t}\n\n\t// 检查限流状态\n\tif atomic.LoadInt32(&p.rateLimited) == 1 {\n\t\tif p.debugMode {\n\t\t\tlog.Printf(\"[YUHUAGE] 当前处于限流状态，跳过搜索\")\n\t\t}\n\t\treturn nil, fmt.Errorf(\"rate limited\")\n\t}\n\n\t// 构建搜索URL\n\tencodedQuery := url.QueryEscape(keyword)\n\tsearchURL := fmt.Sprintf(\"%s%s%s-%d-time.html\", BaseURL, SearchPath, encodedQuery, 1)\n\t\n\t// 创建带超时的上下文\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\t\n\t// 创建请求对象\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", searchURL, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"[%s] 创建请求失败: %w\", p.Name(), err)\n\t}\n\t\n\t// 设置请求头\n\treq.Header.Set(\"User-Agent\", UserAgent)","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/fish2018/pansou/blob/beaa56133755a548ebc51b090b3816e2ae044aa6/plugin/yuhuage/yuhuage.go#L54-L90","documentation":"The yuhuage plugin tracks a rateLimited flag set when a previous request received HTTP 429. While that flag is set (60 seconds), searchImpl short-circuits and returns \"rate limited\" without making any network request. It is a client-side circuit breaker, not the server's 429 itself.","triggerScenarios":"A Search/SearchWithResult call on YuhuagePlugin arrives while p.rateLimited == 1, i.e. within 60 seconds after any earlier request got a 429 from the upstream site.","commonSituations":"High search volume triggers the upstream site's rate limiting; many concurrent user searches fan out to this plugin; after one 429, all searches for the next minute fail fast with this error.","solutions":["Wait 60 seconds for the flag to reset, or reduce overall request rate to the plugin.","Check debug logs (\"当前处于限流状态，跳过搜索\") to confirm the circuit breaker is active.","Lower concurrency or add request pacing so the upstream never returns 429 in the first place.","Treat this error as transient in the caller and skip/backoff rather than alerting."],"exampleFix":"// before\nif atomic.LoadInt32(&p.rateLimited) == 1 {\n    return nil, fmt.Errorf(\"rate limited\")\n}\n// after\nif atomic.LoadInt32(&p.rateLimited) == 1 {\n    time.Sleep(time.Until(rateLimitResetAt.Load()))\n    if atomic.LoadInt32(&p.rateLimited) == 1 {\n        return nil, fmt.Errorf(\"rate limited\")\n    }\n}","handlingStrategy":"retry","validationCode":"// no pre-call validation possible; the flag is internal.\n// Instead, pace your calls:\nif time.Since(lastYuhuageCall) < 2*time.Second {\n    time.Sleep(2*time.Second - time.Since(lastYuhuageCall))\n}","typeGuard":null,"tryCatchPattern":"results, err := plugin.Search(keyword, ext)\nif err != nil {\n    if err.Error() == \"rate limited\" {\n        time.Sleep(65 * time.Second)\n        results, err = plugin.Search(keyword, ext)\n    }\n}","preventionTips":["Throttle search frequency so upstream never returns 429.","Treat 'rate limited' as transient and cache/skip for the 60s window.","Run a single shared instance rather than many clients hitting the same IP."],"tags":["rate-limit","http","throttling"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"beaa56133755a548ebc51b090b3816e2ae044aa6","analyzedAt":"2026-09-07T00:31:18.025Z","contentChangedAt":"2026-09-07T00:31:18.025Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}