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

  1. Read {last_error} in the message to identify the real per-attempt failure before changing anything.
  2. Increase the attempts count and/or the interval between attempts so transient 429/5xx and network blips are absorbed.
  3. 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.
  4. Check network egress to api.virustotal.com (proxy, DNS, TLS) from the environment running the script.
  5. 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

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


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/347c022b97a83685. Report an issue: GitHub.