Skip to content

RateLimited error discards Retry-After; rate limiter back-off never engages #431

Description

@forkwright

Finding

SearchIndexerError::RateLimited { retry_after_seconds } is produced by fetch_xml when an indexer returns HTTP 429 with a Retry-After header, but handle_search_error matches RateLimited only under the _ => None wildcard. It performs no status update and never calls rate_limiter.set_retry_after(), so the parsed retry_after_seconds is discarded. The set_retry_after method and the retry_after field on TokenBucket are dead in production: the wiring from the HTTP 429 response to the rate limiter's back-off was never completed.

Evidence

crates/zetesis/src/search.rs:238RateLimited falls through to:

_ => None,

RateLimited is constructed with the parsed header value at crates/zetesis/src/client/torznab.rs:75-79:

return Err(SearchIndexerError::RateLimited {
    indexer_id: self.config.id,
    retry_after_seconds: retry_after,  // parsed from Retry-After header
    ...
});

set_retry_after is defined at crates/zetesis/src/rate_limit.rs:94 but is never called from production code.

Why this matters

After a 429, the next search immediately re-queries the same indexer, draws another 429, and repeats indefinitely. Under the counter-surveillance threat model this is doubly harmful: it hammers the indexer (risking account suspension and loss of a search source), and the tight, repetitive 429-storm is a distinctive, fingerprintable traffic pattern toward the indexer that undermines the operator's low-profile posture. The explicitly parsed Retry-After value is silently thrown away.

Desired correction

Add a RateLimited arm to handle_search_error that calls self.rate_limiter.set_retry_after(indexer.id, Duration::from_secs(retry_after_seconds)), with a bounded cap (e.g. 1 hour) on the maximum back-off.

Done when: a test demonstrates that after a RateLimited error with retry_after_seconds=60, the next acquire() is delayed by approximately 60 seconds.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions