stamparm/maltrail · error · RuntimeError
( )
Error message
%s (%s): %s
What it means
In check_redistribution.py, main() iterates SOURCES (provider, url, kind) and calls fetch(url) for each redistribution-list provider. Any exception raised while fetching (network failure, DNS error, HTTP error, TLS problem) is caught and re-raised as a RuntimeError formatted '%s (%s): %s' — provider name, URL, and the underlying exception — so the caller knows exactly which source could not be retrieved and why.
Solutions
- Read the inner exception in the message to identify the cause (connection refused, timeout, HTTP status) and fix network access or proxy settings.
- Verify the provider URL in SOURCES is still valid — open it in a browser or curl it, and update it if the provider moved its feed.
- Re-run later if the failure is a transient upstream outage; consider adding retry logic around fetch.
- In restricted environments, run from a machine with egress access or pre-download the lists and adapt fetch to read from local files.
Defensive patterns
Strategy: try-catch
Validate before calling
import urllib.request
def reachable(url):
try:
with urllib.request.urlopen(url, timeout=10) as r:
return r.status == 200
except Exception:
return False
for provider, url, kind in SOURCES:
if not reachable(url):
print(f"skipping {provider}: {url} unreachable") Try / catch
try:
body = fetch(url)
except RuntimeError as ex:
provider, url, cause = ex.args[0] if False else str(ex).split(' (', 1)
log.warning("source fetch failed, will retry/fallback: %s", ex)
body = cached_or_skip(provider) Prevention
- Pre-check provider URLs with a lightweight HEAD request before running the full redistribution check.
- Cache successful fetches so a single transient outage doesn't fail the whole run.
- Set explicit timeouts in fetch and add bounded retries with backoff for flaky providers.
- Monitor provider feed URLs (they move or get deprecated) and keep the SOURCES list up to date.
- Run network-dependent checks from environments with confirmed egress; skip or mock in air-gapped CI.
When it happens
Trigger: fetch(url) raises any Exception for one of the SOURCES entries: the URL is unreachable, DNS resolution fails, the server returns a non-200 status that fetch turns into an exception, the connection times out, or TLS handshake fails.
Common situations: Running the check in a CI container or air-gapped network without internet access; a provider changing or deprecating its URL; a corporate proxy or firewall blocking the host; transient upstream outage of a blocklist provider (e.g. a CDN or government feed being temporarily down).
Understand the failure class
Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.
Related errors
AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13).
Data as JSON: /api/errors/83aa777eb48cdede.
Report an issue: GitHub.
Appendix: source
Thrown at sensor/tools/check_redistribution.py:103
# Piles whose whole purpose is to name shared infrastructure. update_trails() spares them and so
# does this: reporting them would be reporting the intent.
EXEMPT_INFO = ("parking", "sinkhole", "cdn", "mass scanner")
def fetch(url):
request = Request(url, headers={"User-agent": "maltrail-redistribution-check"})
return urlopen(request, timeout=TIMEOUT).read().decode("utf8", "replace")
def networks():
"""{provider: [ip_network, ...]} from the providers' own published lists."""
retval = {}
for provider, url, kind in SOURCES:
try:
body = fetch(url)
except Exception as ex:
raise RuntimeError("%s (%s): %s" % (provider, url, ex))
prefixes = []
if kind == "lines":
prefixes = [_.strip() for _ in body.splitlines() if _.strip()]
elif kind == "aws":
data = json.loads(body)
prefixes = [_["ip_prefix"] for _ in data.get("prefixes", [])
if _.get("service") in AWS_SHARED_SERVICES]
prefixes += [_["ipv6_prefix"] for _ in data.get("ipv6_prefixes", [])
if _.get("service") in AWS_SHARED_SERVICES]
elif kind == "gcp":
data = json.loads(body)
for entry in data.get("prefixes", []):
prefixes.append(entry.get("ipv4Prefix") or entry.get("ipv6Prefix"))
elif kind == "fastly":
data = json.loads(body)
prefixes = list(data.get("addresses", [])) + list(data.get("ipv6_addresses", []))
View on GitHub (pinned to 77cfb06d76)