Z4nzu/hackingtool · warning · RateLimited
GitHub search rate limit reached{wait}
Error message
GitHub search rate limit reached{wait} What it means
RateLimited is raised by hackingtool's GitHub repository search (_fetch in discover.py:502) when the GitHub Search API responds with HTTP 403 or 429 and the response header x-ratelimit-remaining is exactly "0" — i.e. the caller has exhausted GitHub's primary rate limit for the search endpoint. The library deliberately rethrows this as a distinct exception (instead of returning [] like other failures) so callers can tell 'no results / transient error' apart from 'you must wait'. The message embeds a computed wait hint derived from the x-ratelimit-reset header, e.g. "GitHub search rate limit reached (~58s)".
Source
Thrown at src/hackingtool/discover.py:523
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": _USER_AGENT,
})
tok = _token()
if tok:
req.add_header("Authorization", f"Bearer {tok}")
try:
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
if exc.code in (403, 429) and exc.headers.get("x-ratelimit-remaining") == "0":
reset = exc.headers.get("x-ratelimit-reset", "")
wait = ""
try:
wait = f" (~{max(0, int(reset) - int(time.time()))}s)"
except ValueError:
pass
raise RateLimited(f"GitHub search rate limit reached{wait}") from exc
raise
def _to_repo(item: dict) -> Repo:
"""Read ONLY the allowlisted fields. Nothing else is touched."""
lic = item.get("license") or {}
owner = item.get("owner") or {}
return Repo(
full_name=item.get("full_name", ""),
description=(item.get("description") or "").strip(),
url=item.get("html_url", ""),
stars=int(item.get("stargazers_count") or 0),
forks=int(item.get("forks_count") or 0),
pushed_at=item.get("pushed_at") or "",
created_at=item.get("created_at") or "",
archived=bool(item.get("archived")) or bool(item.get("disabled")),
fork=bool(item.get("fork")),
license=(lic.get("spdx_id") or "") if lic.get("spdx_id") != "NOASSERTION" else "",View on GitHub (pinned to 9b92b6156d)
Solutions
- Set a GitHub token: export HACKINGTOOL_GITHUB_TOKEN (or GITHUB_TOKEN / GH_TOKEN) — this raises the search limit from 10 to 30 requests/minute; create one at https://github.com/settings/tokens (no scopes needed for public search).
- Wait out the window shown in the message (the "(~Ns)" suffix is seconds until x-ratelimit-reset) and retry the same query — the 24h on-disk cache means a successful retry costs nothing on repeat.
- Reduce distinct queries: re-run the same wording instead of new phrasings, since cache hits never touch the network.
- If the token is set but the error persists, verify it is valid (gh auth status or curl -H "Authorization: Bearer $TOKEN" https://api.github.com/rate_limit) — an invalid token silently downgrades you to the anonymous quota.
- In scripts/loops, wrap calls in a RateLimited handler that sleeps for the reported seconds and retries once (see tryCatchPattern).
Example fix
# before: unauthenticated, 10 search req/min $ hackingtool find "xss scanner" $ hackingtool find "sql injection scanner" $ hackingtool find "subdomain scanner" # -> RateLimited: GitHub search rate limit reached (~52s) # after: authenticated, 30 req/min $ export HACKINGTOOL_GITHUB_TOKEN=ghp_your_personal_access_token $ hackingtool find "xss scanner"
Defensive patterns
Strategy: retry
Validate before calling
import json, os, time, urllib.request
def github_search_quota_left() -> int | None:
"""Check https://api.github.com/rate_limit before searching. None = unknown."""
tok = (os.environ.get("HACKINGTOOL_GITHUB_TOKEN")
or os.environ.get("GITHUB_TOKEN")
or os.environ.get("GH_TOKEN") or "").strip()
req = urllib.request.Request("https://api.github.com/rate_limit")
if tok:
req.add_header("Authorization", f"Bearer {tok}")
try:
with urllib.request.urlopen(req, timeout=10) as r:
core = json.load(r)["resources"]["search"]
return core["remaining"]
except Exception:
return None
if github_search_quota_left() == 0:
raise SystemExit("search quota exhausted; wait or set HACKINGTOOL_GITHUB_TOKEN")
from hackingtool import discover
discover._search("topic:xss") Type guard
from hackingtool import discover
def is_rate_limited(exc: BaseException) -> bool:
"""True if exc is hackingtool's GitHub search rate-limit error."""
return isinstance(exc, discover.RateLimited) Try / catch
import re, time
from hackingtool import discover
_WAIT_RX = re.compile(r"~(\d+)s")
try:
repos = discover._search("topic:security language:python")
except discover.RateLimited as exc:
m = _WAIT_RX.search(str(exc))
delay = int(m.group(1)) + 2 if m else 60 # header-derived wait, safe floor
time.sleep(delay)
repos = discover._search("topic:security language:python") # cache makes retry cheap
except Exception:
repos = [] # other failures degrade gracefully
# proceed with repos Prevention
- Always export HACKINGTOOL_GITHUB_TOKEN (a fine-grained token with no scopes works for public search) in shells, CI env, and systemd units that run hackingtool find.
- Batch exploratory queries instead of firing many phrasings back-to-back; unauthenticated search allows only 10 requests/minute.
- Rely on the built-in 24h per-query cache: repeat identical wording rather than generating new variants of the same query.
- Check https://api.github.com/rate_limit (search bucket) before long batch jobs and abort early with a clear message instead of mid-run.
- Catch RateLimited specifically and sleep for the reported "~Ns" wait hint plus a small buffer, then retry once — never blanket-retry on generic HTTPError, since that hides real failures.
- In multi-worker setups, share one token and a shared cache directory so workers don't each burn the anonymous IP quota.
When it happens
Trigger: Any call chain that reaches discover._search(query) with a cache miss (the 24h on-disk cache in USER_CONFIG_FILE.parent/cache/find/ is empty or expired for that query): it builds a request to the GitHub search/repositories endpoint with per_page=10, and GitHub replies 403 (unauthenticated) or 429 with x-ratelimit-remaining: 0. Without a token the search limit is only 10 requests/minute, so ~10 distinct queries in a minute trigger it; with a token it is 30/minute. The 403+remaining=0 combination is how GitHub reports exhausted unauthenticated quotas (429 is the authenticated form), which is why both codes are checked in discover.py:516.
Common situations: Running hackingtool find repeatedly with different phrasings in a short session while unauthenticated (most common). CI or shared NAT/proxy egress where the 60-requests/hour anonymous pool is consumed by others on the same IP. Providing a GitHub token that is expired, revoked, or lacks access, causing GitHub to treat requests as anonymous. Clock skew or an old cached process holding a stale token. Multi-user scripts or loops that call the search CLI per-item without backoff.
AI-assisted analysis of Z4nzu/hackingtool@9b92b6156d (2026-08-14).
Data as JSON: /api/errors/b13a4a6398c787e9.
Report an issue: GitHub.