fix(ratelimit): honour -rl global fallback and -rls duration (#1434)#1764
Conversation
…discovery#1434) Two bugs in rate limit handling: 1. Global -rl flag was ignored for sources without a per-source override. Only sources explicitly listed in -rls got rate-limited; all others ran unlimited, defeating the purpose of -rl. 2. Per-source duration from -rls (e.g. hackertarget=2/m) was silently dropped. runner.go stored MaxCount but discarded Duration, so all sources defaulted to per-second regardless of the /m or /s suffix. Root cause: runner.go converted goflags.RateLimitMap → CustomRateLimit but only copied MaxCount. buildMultiRateLimiter only checked per-source limits, never falling back to the global rate. Changes: - Add CustomDuration field to CustomRateLimit struct - Store both MaxCount and Duration from -rls in runner.go - Add resolveSourceRateLimit() with clear priority: per-source > global > unlimited - Guard against nil rateLimit in buildMultiRateLimiter - Add 9 unit tests covering all combinations Fixes projectdiscovery#1434
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughAdds per-source rate-limit durations and resolution logic: a new CustomDuration map in CustomRateLimit, a resolveSourceRateLimit helper, updates to passive rate-limiter construction to use resolved (limit, duration), runner wiring to populate durations, and tests covering nil, per-source, global, default, and unlimited cases. Changes
Sequence Diagram(s)sequenceDiagram
participant Runner
participant Subscraping as CustomRateLimit
participant Passive
participant RateLimiter as MultiRateLimiter
Runner->>Subscraping: populate Custom (limits) and CustomDuration (per-source durations)
Runner->>Passive: provide configured CustomRateLimit
Passive->>Passive: resolveSourceRateLimit(source) => (limit, duration)
Passive->>RateLimiter: addRateLimiter(source, limit, duration)
RateLimiter-->>Passive: success / error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
pkg/passive/ratelimit_test.go (2)
64-69: Test name is misleading: tests empty struct, notnilpointer.The test is named
TestResolveSourceRateLimit_NilRateLimitbut passes&subscraping.CustomRateLimit{}(a zero-valued struct with nil internal maps), not an actualnilpointer. This distinction matters because:
- The function doesn't receive a nil pointer here
- The zero-valued struct has nil
Mapfields inside, which may panicConsider renaming to
TestResolveSourceRateLimit_EmptyRateLimitorTestResolveSourceRateLimit_ZeroValuedRateLimitto accurately describe what's being tested.♻️ Suggested rename
-func TestResolveSourceRateLimit_NilRateLimit(t *testing.T) { - // nil CustomRateLimit should not panic +func TestResolveSourceRateLimit_ZeroValuedRateLimit(t *testing.T) { + // Zero-valued CustomRateLimit (with nil internal maps) should not panic rl, dur := resolveSourceRateLimit(5, &subscraping.CustomRateLimit{}, "crtsh")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/passive/ratelimit_test.go` around lines 64 - 69, Rename the test TestResolveSourceRateLimit_NilRateLimit to accurately reflect that it passes a zero-valued struct (not a nil pointer); for example TestResolveSourceRateLimit_EmptyRateLimit or TestResolveSourceRateLimit_ZeroValuedRateLimit, and update the test name string accordingly where the function resolveSourceRateLimit is invoked with &subscraping.CustomRateLimit{} so readers understand it is testing the empty/zero-valued struct case rather than a nil pointer.
115-125: Remove dead code reference.Line 124 (
_ = math.MaxUint32) serves no purpose—it doesn't assert anything and the comment "referenced for clarity" doesn't clarify the test behavior. Consider removing it or adding a meaningful assertion about the unlimited behavior.♻️ Proposed cleanup
func TestBuildMultiRateLimiter_UnlimitedWhenNoLimits(t *testing.T) { // Without any rate limits, sources should get MaxUint32 (effectively unlimited) agent := New([]string{"hackertarget"}, []string{}, false, false) crl := newCustomRateLimit(nil, nil) limiter, err := agent.buildMultiRateLimiter(context.Background(), 0, crl) require.NoError(t, err) require.NotNil(t, limiter) - // No assertion on internal state - just verify it doesn't error - _ = math.MaxUint32 // referenced for clarity }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/passive/ratelimit_test.go` around lines 115 - 125, The test TestBuildMultiRateLimiter_UnlimitedWhenNoLimits contains a dead reference "_ = math.MaxUint32" that should be removed or replaced; locate the test and either delete the pointless line or add a meaningful assertion that verifies the returned limiter from agent.buildMultiRateLimiter (created with newCustomRateLimit(nil, nil)) grants unlimited behavior (e.g., check limiter is non-nil and/or its per-source limit equals math.MaxUint32) and remove the unused math.MaxUint32 assignment if you opt to keep the test minimal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/passive/passive.go`:
- Around line 88-90: The nil guard for rateLimit creates a zero-valued
CustomRateLimit whose Custom.Map and CustomDuration.Map remain nil and can panic
when resolveSourceRateLimit calls .Get() (SyncLockMap.Get) on them; modify the
initialization of rateLimit in pkg/passive/passive.go (the block that sets
rateLimit = &subscraping.CustomRateLimit{}) to allocate non-nil SyncLockMap
instances for both Custom.Map and CustomDuration.Map following the pattern used
in runner.go (use mapsutil.NewSyncLockMap() or the project-equivalent
constructor) and add the mapsutil import if missing so resolveSourceRateLimit
and SyncLockMap.Get() never operate on nil maps.
---
Nitpick comments:
In `@pkg/passive/ratelimit_test.go`:
- Around line 64-69: Rename the test TestResolveSourceRateLimit_NilRateLimit to
accurately reflect that it passes a zero-valued struct (not a nil pointer); for
example TestResolveSourceRateLimit_EmptyRateLimit or
TestResolveSourceRateLimit_ZeroValuedRateLimit, and update the test name string
accordingly where the function resolveSourceRateLimit is invoked with
&subscraping.CustomRateLimit{} so readers understand it is testing the
empty/zero-valued struct case rather than a nil pointer.
- Around line 115-125: The test TestBuildMultiRateLimiter_UnlimitedWhenNoLimits
contains a dead reference "_ = math.MaxUint32" that should be removed or
replaced; locate the test and either delete the pointless line or add a
meaningful assertion that verifies the returned limiter from
agent.buildMultiRateLimiter (created with newCustomRateLimit(nil, nil)) grants
unlimited behavior (e.g., check limiter is non-nil and/or its per-source limit
equals math.MaxUint32) and remove the unused math.MaxUint32 assignment if you
opt to keep the test minimal.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 65c3b523-7e03-4481-b3ad-8e2ac96937ff
📒 Files selected for processing (4)
pkg/passive/passive.gopkg/passive/ratelimit_test.gopkg/runner/runner.gopkg/subscraping/types.go
Neo - PR Security ReviewCaution Review could not be completed Review could not be completed. Please retry with Suggestion: Try again with Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pkg/passive/ratelimit_test.go (1)
115-125: Remove dead code: themath.MaxUint32reference is a no-op.Line 124 assigns to the blank identifier and has no effect. The comment says "referenced for clarity" but this doesn't verify any behavior. Either add an actual assertion or remove this line along with the unused
mathimport.🧹 Proposed cleanup
import ( "context" - "math" "testing" "time"func TestBuildMultiRateLimiter_UnlimitedWhenNoLimits(t *testing.T) { // Without any rate limits, sources should get MaxUint32 (effectively unlimited) agent := New([]string{"hackertarget"}, []string{}, false, false) crl := newCustomRateLimit(nil, nil) limiter, err := agent.buildMultiRateLimiter(context.Background(), 0, crl) require.NoError(t, err) require.NotNil(t, limiter) - // No assertion on internal state - just verify it doesn't error - _ = math.MaxUint32 // referenced for clarity }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/passive/ratelimit_test.go` around lines 115 - 125, In TestBuildMultiRateLimiter_UnlimitedWhenNoLimits remove the dead no-op reference to math.MaxUint32 (the line assigning it to the blank identifier) and delete the now-unused math import; if you intended to assert behavior instead, replace that line with a concrete assertion on the limiter (e.g., expected rate/limit) inside the same test function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@pkg/passive/ratelimit_test.go`:
- Around line 115-125: In TestBuildMultiRateLimiter_UnlimitedWhenNoLimits remove
the dead no-op reference to math.MaxUint32 (the line assigning it to the blank
identifier) and delete the now-unused math import; if you intended to assert
behavior instead, replace that line with a concrete assertion on the limiter
(e.g., expected rate/limit) inside the same test function.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8da0b4fb-6580-424c-8e6d-c5901e2e56ee
📒 Files selected for processing (1)
pkg/passive/ratelimit_test.go
|
Fixed in latest commit: properly initialize the rateLimit = &subscraping.CustomRateLimit{
Custom: mapsutil.SyncLockMap[string, uint]{
Map: make(map[string]uint),
},
CustomDuration: mapsutil.SyncLockMap[string, time.Duration]{
Map: make(map[string]time.Duration),
},
}This mirrors the initialization pattern used in |
CodeRabbit identified that the zero-valued CustomRateLimit struct left Custom.Map and CustomDuration.Map as nil, which could cause a panic when resolveSourceRateLimit calls .Get() on these maps. Mirror the initialization pattern from runner.go by explicitly creating the underlying maps.
CharlesWong
left a comment
There was a problem hiding this comment.
Thanks for catching this @coderabbitai! The nil map panic risk was real — I've applied the fix to properly initialize both Custom.Map and CustomDuration.Map using the same pattern from runner.go. Pushed in cca10d8.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
pkg/passive/passive.go (2)
89-98: CentralizeCustomRateLimitinitialization.This struct literal now exists here and in
pkg/runner/runner.go. SinceCustomDurationwas just added, multiple hand-written initializers make the next field addition easy to miss again. A small constructor orensureInitialized()helper inpkg/subscrapingwould keep that contract in one place.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/passive/passive.go` around lines 89 - 98, The code duplicates construction of subscraping.CustomRateLimit (with fields Custom and CustomDuration using mapsutil.SyncLockMap) in multiple places; add a single constructor or ensureInitialized() function in the subscraping package (e.g., func NewCustomRateLimit() *CustomRateLimit or func (r *CustomRateLimit) EnsureInitialized()) that returns/initializes a CustomRateLimit with both Custom and CustomDuration maps set, then replace the inline struct literal here and in pkg/runner/runner.go with a call to that constructor/EnsureInitialized to centralize initialization and avoid future omissions.
121-126: Normalize custom source keys consistently.The lookup lowercases
sourceName, butWithCustomRateLimitcallers populateCustomandCustomDurationdirectly, andpkg/runner/runner.gocurrently stores parsed keys as-is. That leaves a hidden casing contract where mixed-case entries silently miss and fall back to the global/unlimited path. Normalize keys on insert as well, or keep lookups case-sensitive and document the expected format.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/passive/passive.go` around lines 121 - 126, The code lowercases sourceName for lookups (sourceLower) but callers like WithCustomRateLimit populate rateLimit.Custom and rateLimit.CustomDuration without normalizing keys, causing mismatches; update the code path that inserts custom keys (e.g., the WithCustomRateLimit constructor or the runner code that parses and stores keys) to normalize keys to strings.ToLower(...) on insert so that rateLimit.Custom.Get(sourceLower) and rateLimit.CustomDuration.Get(sourceLower) will match, or alternatively make the lookup use the exact stored key consistently—ensure the change touches the insertion helpers (WithCustomRateLimit and the runner's parsing/storing logic) and add a small comment documenting the lowercase contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@pkg/passive/passive.go`:
- Around line 89-98: The code duplicates construction of
subscraping.CustomRateLimit (with fields Custom and CustomDuration using
mapsutil.SyncLockMap) in multiple places; add a single constructor or
ensureInitialized() function in the subscraping package (e.g., func
NewCustomRateLimit() *CustomRateLimit or func (r *CustomRateLimit)
EnsureInitialized()) that returns/initializes a CustomRateLimit with both Custom
and CustomDuration maps set, then replace the inline struct literal here and in
pkg/runner/runner.go with a call to that constructor/EnsureInitialized to
centralize initialization and avoid future omissions.
- Around line 121-126: The code lowercases sourceName for lookups (sourceLower)
but callers like WithCustomRateLimit populate rateLimit.Custom and
rateLimit.CustomDuration without normalizing keys, causing mismatches; update
the code path that inserts custom keys (e.g., the WithCustomRateLimit
constructor or the runner code that parses and stores keys) to normalize keys to
strings.ToLower(...) on insert so that rateLimit.Custom.Get(sourceLower) and
rateLimit.CustomDuration.Get(sourceLower) will match, or alternatively make the
lookup use the exact stored key consistently—ensure the change touches the
insertion helpers (WithCustomRateLimit and the runner's parsing/storing logic)
and add a small comment documenting the lowercase contract.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 49182a5d-6214-4fd2-9f94-2af5c8346c8e
📒 Files selected for processing (1)
pkg/passive/passive.go
Per CodeRabbit nitpick: the blank identifier assignment and math import in TestBuildMultiRateLimiter_UnlimitedWhenNoLimits served no purpose. The test already verifies correct behavior via NoError + NotNil assertions.
|
Three nitpicks addressed (cf0b801):
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pkg/passive/ratelimit_test.go (1)
82-90: Consider adding behavioral assertions.The test verifies that
buildMultiRateLimitercompletes without error, but doesn't validate that the global rate limit (5) was actually applied to both sources. If theMultiLimiterAPI allows inspecting configured limits, consider adding assertions to verify the expected behavior.If the API doesn't expose this, the current smoke test approach is acceptable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/passive/ratelimit_test.go` around lines 82 - 90, TestBuildMultiRateLimiter_GlobalAppliedToAll currently only verifies no error from agent.buildMultiRateLimiter but doesn't assert the global limit was applied; update the test to inspect the returned limiter (from buildMultiRateLimiter) and assert that each source's configured rate equals 5 by using the MultiLimiter inspection API (e.g., any GetLimit/GetRate/GetLimiterForSource or exported per-source limiter methods) or unwrap the MultiLimiter to its child limiters and assert their limits; if the MultiLimiter API doesn't expose limits, add a short exercise that sends known-numbered requests per-source and asserts throttling behavior consistent with rate=5, otherwise leave the smoke test as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@pkg/passive/ratelimit_test.go`:
- Around line 82-90: TestBuildMultiRateLimiter_GlobalAppliedToAll currently only
verifies no error from agent.buildMultiRateLimiter but doesn't assert the global
limit was applied; update the test to inspect the returned limiter (from
buildMultiRateLimiter) and assert that each source's configured rate equals 5 by
using the MultiLimiter inspection API (e.g., any
GetLimit/GetRate/GetLimiterForSource or exported per-source limiter methods) or
unwrap the MultiLimiter to its child limiters and assert their limits; if the
MultiLimiter API doesn't expose limits, add a short exercise that sends
known-numbered requests per-source and asserts throttling behavior consistent
with rate=5, otherwise leave the smoke test as-is.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 328c427b-e45b-44e5-abdf-af5f53859974
📒 Files selected for processing (1)
pkg/passive/ratelimit_test.go
- PerSourceExceedsGlobal: per-source limit > global should be honoured (verifies sourceRateLimitOrDefault returns source, not global) - CaseInsensitive: source names stored lowercase must match mixed-case lookups (documents and verifies the ToLower normalization contract)
|
Senior dev review — two more test cases added (240305b):
|
|
Acknowledged @coderabbitai[bot]'s review. |
|
Acknowledged @neo-by-projectdiscovery-dev[bot]'s review. |
Rate Limit Exceeded
|
|
@dogancanbakir This PR fixes #1434 (both |
|
@Mzack9999 Thanks for merging this PR! Could you help process the $75 bounty reward for issue #1434? I've posted |
|
/tip $75 |
|
🎉🎈 @CharlesWong has been awarded $75 by ProjectDiscovery! 🎈🎊 |
Summary
Fixes #1434 — both
-rl(global rate limit) and-rls(per-source rate limit with duration) were silently broken.Root Cause
Two independent bugs that no single existing approach fully addresses:
-rlignored for most sources:buildMultiRateLimiter()only checked per-source overrides from-rls. Sources without an explicit-rlsentry gotrl=0→ unlimited, even when-rl 5was set.-rlsduration discarded:runner.goconvertedgoflags.RateLimitMap→CustomRateLimitbut only storedMaxCount, droppingDuration. So-rls hackertarget=2/msilently became2/s.Why This PR
I analyzed all existing approaches and found each only partially fixes the issue:
-rlfallback-rlsduration preservedCustomDurationrunner.gostores from goflagsbuildMultiRateLimiterresolveSourceRateLimit()Key insight: The duration bug lives in
runner.go(where-rlsis parsed), not justpassive.go. Most PRs only patchpassive.goand miss the root cause.Changes
pkg/subscraping/types.goCustomDurationfield toCustomRateLimitstructpkg/runner/runner.goMaxCountandDurationfrom-rlsflag parsingpkg/passive/passive.goresolveSourceRateLimit()with clear priority: per-source → global → unlimited-rlswhen set, default to per-secondrateLimitparameterpkg/passive/ratelimit_test.go(new, 9 tests)-rlfallback for unlisted sourcesbuildMultiRateLimiterHow it works now
Testing
Summary by CodeRabbit
Bug Fixes
Tests
Chores