{"record":{"id":"8c61d34a1b9a2339","repo":"invoke-ai/InvokeAI","slug":"job-was-cancelled-at-caller-s-request","errorCode":null,"errorMessage":"Job was cancelled at caller's request","messagePattern":"Job was cancelled at caller's request","errorType":"exception","errorClass":"DownloadJobCancelledException","httpStatus":null,"severity":"info","filePath":"invokeai/app/services/download/download_default.py","lineNumber":608,"sourceCode":"        elif resp.status_code != 200:\n            host = urlparse(str(resp.url or url)).netloc\n            status = resp.status_code\n            reason = resp.reason\n            if status >= 500:\n                self._logger.error(f\"Remote server error from {host}: HTTP {status} {reason}\")\n                raise HTTPError(reason)\n            self._logger.error(f\"Download failed from {host}: HTTP {status} {reason}\")\n            raise HTTPError(reason)\n\n        self._logger.debug(f\"{job.source}: Downloading {job.download_path}\")\n        report_delta = job.total_bytes / 100  # report every 1% change\n        last_report_bytes = 0\n\n        # DOWNLOAD LOOP\n        with open(in_progress_path, open_mode) as file:\n            for data in resp.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):\n                if job.cancelled:\n                    raise DownloadJobCancelledException(\"Job was cancelled at caller's request\")\n\n                job.bytes += file.write(data)\n                if (job.bytes - last_report_bytes >= report_delta) or (job.bytes >= job.total_bytes):\n                    last_report_bytes = job.bytes\n                    self._signal_job_progress(job)\n\n        if job.total_bytes > 0 and job.bytes < job.total_bytes:\n            job.resume_required = True\n            job.resume_message = \"Download interrupted. Resume required.\"\n            job.pause()\n            raise DownloadJobCancelledException(\"Download interrupted. Resume required.\")\n\n        # if we get here we are done and can rename the file to the original dest\n        self._logger.debug(f\"{job.source}: saved to {job.download_path} (bytes={job.bytes})\")\n        in_progress_path.rename(job.download_path)\n\n    def _validate_url(self, url: str) -> None:\n        \"\"\"Refuse to fetch URLs that point at addresses only the server can reach.\"\"\"","sourceCodeStart":590,"sourceCodeEnd":626,"githubUrl":"https://github.com/invoke-ai/InvokeAI/blob/0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06/invokeai/app/services/download/download_default.py#L590-L626","documentation":"Inside the streaming download loop, each chunk first checks job.cancelled. If the caller requested cancellation (via the queue's cancel API), the loop aborts immediately by raising DownloadJobCancelledException('Job was cancelled at caller's request'). The partial `.downloading` file is left in place so a later resume can continue from the last chunk. This is expected control flow, not a malfunction.","triggerScenarios":"Calling RemoteDownloadService.cancel(job_id) — or cancelling the source/queue — while bytes are actively streaming in _do_download's iter_content loop.","commonSituations":"User cancels a slow large-model download from the UI; app shutdown drains the queue and cancels in-flight jobs; a script aborts a download after a timeout; duplicate job cancelled in favor of another.","solutions":["No fix needed: handle DownloadJobCancelledException as a normal cancellation; the partial `.downloading` file supports later resume.","If the cancellation was unintentional, resume the job — it continues from job.bytes without redownloading everything.","During shutdown, wait for jobs or call the queue's graceful-stop API instead of hard-cancel to finish small downloads.","Clean up orphaned `.downloading` files only if you never intend to resume."],"exampleFix":"// before: treating cancellation as fatal\ntry:\n    queue.join()\nexcept Exception as e:\n    logger.critical(e)\n\n// after: distinguish cancellation\ntry:\n    queue.join()\nexcept DownloadJobCancelledException:\n    logger.info('download cancelled; resumable later')","handlingStrategy":"try-catch","validationCode":"# check before cancelling whether the job is nearly done\nif job.total_bytes and (job.bytes / job.total_bytes) > 0.98:\n    queue.join(job.id)  # let it finish instead of cancelling\nelse:\n    queue.cancel(job.id)","typeGuard":null,"tryCatchPattern":"try:\n    queue.join()\nexcept DownloadJobCancelledException as e:\n    if \"caller's request\" in str(e):\n        logger.info('job cancelled by user; partial .downloading kept for resume')\n    else:\n        logger.warning('job paused for other reason: %s', e)","preventionTips":["Treat DownloadJobCancelledException as expected flow — never log it as a crash.","Check job.cancelled proactively in UI-driven flows before resuming.","Preserve `.downloading` partials so cancelled downloads resume cheaply.","On shutdown, prefer graceful queue stop over immediate cancel for near-complete jobs."],"tags":["download","cancellation","control-flow","resume"],"backgroundTag":"download-cancelled","analyzedSha":"0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06","analyzedAt":"2026-08-29T04:46:49.967Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}