{"record":{"id":"347c022b97a83685","repo":"unslothai/unsloth","slug":"virustotal-upload-failed-after-attempts-attempt","errorCode":null,"errorMessage":"VirusTotal upload failed after {attempts} attempt(s): {last_error}","messagePattern":"VirusTotal upload failed after (.+?) attempt\\(s\\): (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"scripts/virustotal_scan.py","lineNumber":534,"sourceCode":"                    continue\n                raise\n\n            analysis_id = None\n            if isinstance(payload, dict) and isinstance(payload.get(\"data\"), dict):\n                analysis_id = payload[\"data\"].get(\"id\")\n            if not isinstance(analysis_id, str) or not analysis_id:\n                # An accepted upload whose acknowledgement did not parse is a failed\n                # attempt, not a dead end: VirusTotal may well be analysing the file\n                # already. Raising straight out would report the asset unavailable\n                # after we had paid the disclosure cost of sending it, so spend the\n                # remaining attempt on a fresh signed URL instead.\n                last_error = RuntimeError(\"VirusTotal upload did not return an analysis id\")\n                if attempt < attempts:\n                    continue\n                raise last_error\n            return analysis_id\n\n        raise RuntimeError(f\"VirusTotal upload failed after {attempts} attempt(s): {last_error}\")\n\n    def wait_for_analysis(self, analysis_id: str, deadline: float) -> object:\n        \"\"\"Poll until the analysis completes or the caller's deadline passes.\"\"\"\n        while True:\n            # Checked inside request() too, but raising the analysis-specific message\n            # here keeps the summary row readable.\n            if self._clock() >= deadline:\n                raise TimeoutError(f\"analysis {analysis_id} did not complete before the deadline\")\n            _, payload = self.request(\n                \"GET\", f\"{API_ROOT}/analyses/{analysis_id}\", deadline = deadline\n            )\n            attributes = _attributes(payload)\n            if attributes.get(\"status\") == \"completed\":\n                return payload\n            if self._request_interval <= 0:\n                # With throttling disabled (premium key) the loop would otherwise spin.\n                self._sleep(1.0)\n","sourceCodeStart":516,"sourceCodeEnd":552,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/scripts/virustotal_scan.py#L516-L552","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nanalysis_id = vt.upload(path, attempts=1)\n\n# after\nanalysis_id = vt.upload(path, attempts=3)  # absorb transient 429/5xx and network blips","handlingStrategy":"retry","validationCode":"if attempts < 2:\n    raise ValueError('give the VirusTotal upload at least 2-3 attempts for transient failures')","typeGuard":null,"tryCatchPattern":"try:\n    analysis_id = vt.upload(path, attempts=3)\nexcept RuntimeError as e:\n    if 'upload failed after' in str(e):\n        log.error('VirusTotal upload exhausted retries: %s', e)\n        raise","preventionTips":["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."],"tags":["virustotal","network","retry","file-upload"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}