unslothai/unsloth · error · RuntimeError
VirusTotal upload failed after {attempts} attempt(s): {last_
Error message
VirusTotal upload failed after {attempts} attempt(s): {last_error} What it means
RuntimeError raised by scripts/virustotal_scan.py after the upload loop exhausted all attempts without obtaining an analysis id. Each attempt either failed at the HTTP layer (network error, expired signed URL, non-2xx response) or returned an acknowledgement that did not contain a parseable analysis id. The final raise carries the last attempt's error text so the summary row names the real cause.
Source
Thrown at scripts/virustotal_scan.py:534
continue
raise
analysis_id = None
if isinstance(payload, dict) and isinstance(payload.get("data"), dict):
analysis_id = payload["data"].get("id")
if not isinstance(analysis_id, str) or not analysis_id:
# An accepted upload whose acknowledgement did not parse is a failed
# attempt, not a dead end: VirusTotal may well be analysing the file
# already. Raising straight out would report the asset unavailable
# after we had paid the disclosure cost of sending it, so spend the
# remaining attempt on a fresh signed URL instead.
last_error = RuntimeError("VirusTotal upload did not return an analysis id")
if attempt < attempts:
continue
raise last_error
return analysis_id
raise RuntimeError(f"VirusTotal upload failed after {attempts} attempt(s): {last_error}")
def wait_for_analysis(self, analysis_id: str, deadline: float) -> object:
"""Poll until the analysis completes or the caller's deadline passes."""
while True:
# Checked inside request() too, but raising the analysis-specific message
# here keeps the summary row readable.
if self._clock() >= deadline:
raise TimeoutError(f"analysis {analysis_id} did not complete before the deadline")
_, payload = self.request(
"GET", f"{API_ROOT}/analyses/{analysis_id}", deadline = deadline
)
attributes = _attributes(payload)
if attributes.get("status") == "completed":
return payload
if self._request_interval <= 0:
# With throttling disabled (premium key) the loop would otherwise spin.
self._sleep(1.0)
View on GitHub (pinned to 203007d190)
Solutions
- Read {last_error} in the message to identify the real per-attempt failure before changing anything.
- Increase the attempts count and/or the interval between attempts so transient 429/5xx and network blips are absorbed.
- If the last error mentions an expired or rejected URL, verify the signed-URL fetch and the upload happen close together and that the URL is not reused across attempts.
- Check network egress to api.virustotal.com (proxy, DNS, TLS) from the environment running the script.
- If the body parses but has no analysis id, capture and inspect the raw response payload to see whether VirusTotal changed its acknowledgement schema.
Example fix
# before analysis_id = vt.upload(path, attempts=1) # after analysis_id = vt.upload(path, attempts=3) # absorb transient 429/5xx and network blips
Defensive patterns
Strategy: retry
Validate before calling
if attempts < 2:
raise ValueError('give the VirusTotal upload at least 2-3 attempts for transient failures') Try / catch
try:
analysis_id = vt.upload(path, attempts=3)
except RuntimeError as e:
if 'upload failed after' in str(e):
log.error('VirusTotal upload exhausted retries: %s', e)
raise Prevention
- Size the attempt budget and interval to your file size and network reliability.
- Fetch a fresh signed URL per attempt rather than reusing one.
- Log the per-attempt last_error, not just the final raise, to diagnose quickly.
When it happens
Trigger: Calling the VirusTotal upload routine with a stale/expired signed upload URL; network failures or 4xx/5xx from api.virustotal.com on every attempt; a 200 response whose JSON body lacks the analysis id field; retries disabled (attempts=1) so a single transient failure is terminal.
Common situations: CI scanning large assets where the signed URL from the initial fetch expires before the multipart POST finishes; proxy/firewall blocking uploads; VirusTotal rate limiting (429) exceeding the retry budget; throttling misconfigured so all retries happen within one rate-limit window.
Related errors
- Could not refresh ChatGPT authorization. Please retry.
- Could not reach ChatGPT Codex.
- llama-server embedder POST {path} failed after retry
- {label} unavailable for {repo_id}
- Metadata unavailable while resolving GGUF variant '{variant}
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/347c022b97a83685.
Report an issue: GitHub.