5a2fe5279b
Transient DNS resolution failures (socket.gaierror, surfaced as urllib.error.URLError with reason=gaierror) were retried with the generic URLError handler — linear backoff (2s, 4s, 6s) and bounded by the caller-passed `retries` parameter. For callers that pass small retry values (e.g. lib/reddit.py::_subreddit_search uses retries=2), a single first-attempt DNS hiccup followed by one quick retry on the still-flaky resolver would exhaust the retry budget and wipe a whole subreddit sweep — which the caller's broad `except Exception` then silent-empties as `[]`. Fix: - Distinguish URLError-with-gaierror-reason from generic URLError via a new `_is_dns_failure()` helper. - For DNS failures, use exponential backoff (1s, 2s, 4s, ...) instead of the linear default. - For DNS failures, expand the effective retry budget to at least MIN_DNS_RETRIES (=3) on first occurrence, so callers that passed `retries=2` still get a meaningful retry budget for the transient case. Non-DNS URLErrors and HTTPErrors keep the caller's value. - DNS attempts are counted separately (`dns_attempts`) so unrelated URLError or OSError failures within the same call don't accidentally expand the budget further. Reported during a community-signal pass where the Reddit subreddit sweep silently returned zero items after a first-round transient DNS hiccup. The fix lives at the http layer (where the retry loop is) rather than per-source so every caller benefits. Tests: - Verifies a caller-passed retries=2 still gets MIN_DNS_RETRIES=3 attempts on gaierror. - Verifies gaierror-then-success returns successfully on attempt 2. - Verifies the exponential-backoff sleep pattern (1s, 2s) on the retry attempts before exhaustion. - Verifies a non-DNS URLError (ConnectionRefusedError reason) does NOT expand the retry budget — only true DNS failures do. All 12 http tests pass (8 baseline + 4 new). No regressions in the broader test suite (1373 pass / 14 fail, vs 1369 pass / 14 fail on main — the 14 failures are pre-existing and unrelated to this PR).