Skip to content

fix(quota_session): _build_limiter — pass rates list as single arg, not unpacked#14

Open
prezis wants to merge 1 commit into
mainfrom
fix/limiter-multi-rate-unpacking
Open

fix(quota_session): _build_limiter — pass rates list as single arg, not unpacked#14
prezis wants to merge 1 commit into
mainfrom
fix/limiter-multi-rate-unpacking

Conversation

@prezis
Copy link
Copy Markdown
Owner

@prezis prezis commented May 2, 2026

Bug

scraperx.quota_session._build_limiter calls Limiter(*rates) (unpacked), but pyrate-limiter's Limiter(arg) takes a SINGLE positional arg — either a Rate or a List[Rate]. The unpacking silently drops every rate after the first.

For a typical caller like QuotaSession(rates=[Rate(60, Duration.MINUTE), Rate(800, Duration.DAY)]), only the per-minute Rate is enforced; the daily-budget Rate is dropped — daily quota is silently unprotected.

Fix

Pass the list as a single arg:

return Limiter(rates) if len(rates) > 1 else Limiter(rates[0])  # was: Limiter(*rates)

The single-rate fast-path is unchanged.

Verification

After fix:

from pyrate_limiter import Limiter, Rate, Duration
rates = [Rate(60, Duration.MINUTE), Rate(800, Duration.DAY)]
l = Limiter(rates)
print(l.bucket_factory.get_buckets()[0].rates)
# [Rate(limit=60, interval=60000), Rate(limit=800, interval=86400000)]

Both rates are now enforced.

Tests

Adds 3 regression tests in tests/test_quota_session.py that swap in a fake pyrate_limiter module and capture the constructor args:

  • test_build_limiter_passes_multi_rate_list_as_single_arg — locks in the single-arg contract; will fire if the unpacking regression returns.
  • test_build_limiter_passes_single_rate_directly — guards the single-rate fast-path.
  • test_build_limiter_returns_none_for_empty_rates — empty-rates short-circuit.

All 12 quota_session tests pass; full wayback suite still 16/16.

Origin

Discovered by wojak-wojtek s37 day 6 dispatch when adopting QuotaSession across 4 Finnhub fetchers (60/min + 800/day budget). The daily limit was not firing in production — first noticed by a manual bucket_factory.get_buckets()[0].rates inspection during the adoption.

Severity: CRITICAL for any caller passing >1 Rate; no-op for single-rate callers.

Compatibility

  • Backwards-compatible API (no signature change).
  • pyrate-limiter v3 / v4 both accept Limiter(List[Rate]).

🤖 Generated with Claude Code

…ot unpacked

pyrate-limiter's Limiter(arg) takes a SINGLE positional arg (Rate or
List[Rate]). The previous Limiter(*rates) unpacking silently dropped the
daily-budget Rate, leaving only the per-minute rate enforced. Verified by
inspecting bucket_factory.get_buckets()[0].rates after fix — both
60/60000ms and 800/86400000ms rates present.

Discovered by wojak-wojtek s37 day 6 dispatch when adopting QuotaSession
across 4 Finnhub fetchers (60/min + 800/day budget) — daily limit was not
firing in production.

Adds 3 regression tests in test_quota_session.py that capture Limiter
constructor args via a fake module to lock in the single-arg contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant