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:238 — RateLimited falls through to:
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.
Finding
SearchIndexerError::RateLimited { retry_after_seconds }is produced byfetch_xmlwhen an indexer returns HTTP 429 with aRetry-Afterheader, buthandle_search_errormatchesRateLimitedonly under the_ => Nonewildcard. It performs no status update and never callsrate_limiter.set_retry_after(), so the parsedretry_after_secondsis discarded. Theset_retry_aftermethod and theretry_afterfield onTokenBucketare 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:238—RateLimitedfalls through to:RateLimitedis constructed with the parsed header value atcrates/zetesis/src/client/torznab.rs:75-79:set_retry_afteris defined atcrates/zetesis/src/rate_limit.rs:94but 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-Aftervalue is silently thrown away.Desired correction
Add a
RateLimitedarm tohandle_search_errorthat callsself.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
RateLimitederror withretry_after_seconds=60, the nextacquire()is delayed by approximately 60 seconds.