{"record":{"id":"531e5e3473992e13","repo":"langflow-ai/langflow","slug":"cannot-cancel-job-with-status-job-status","errorCode":null,"errorMessage":"Cannot cancel job with status '{job_status}'","messagePattern":"Cannot cancel job with status '(.+?)'","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"src/backend/base/langflow/api/v1/knowledge_bases.py","lineNumber":2299,"sourceCode":"        metadata = KBAnalysisHelper.get_metadata(kb_path, fast=True)\n        asset_id = await _resolve_kb_asset_id(\n            kb_name=kb_name,\n            current_user=current_user,\n            metadata=metadata,\n        )\n\n        # Fetch the latest ingestion job for this KB\n        latest_jobs = await job_service.get_latest_jobs_by_asset_ids([asset_id])\n\n        if asset_id not in latest_jobs:\n            raise HTTPException(status_code=404, detail=f\"No ingestion job found for the knowledge base {kb_name}\")\n\n        job = latest_jobs[asset_id]\n        job_status = job.status.value if hasattr(job.status, \"value\") else str(job.status)\n\n        # Check if job is already completed or failed\n        if job_status in [\"completed\", \"failed\", \"cancelled\", \"timed_out\"]:\n            raise HTTPException(status_code=400, detail=f\"Cannot cancel job with status '{job_status}'\")\n\n        revoked = await task_service.revoke_task(job.job_id)\n        # Update status immediately so background task can see it\n        await job_service.update_job_status(job.job_id, JobStatus.CANCELLED)\n\n        # Clean up any partially ingested chunks from this job. Forward\n        # the KB's configured backend + user_id so non-Chroma KBs\n        # (Mongo/Astra/Postgres) actually find their variable-backed\n        # credentials and delete against the right store — otherwise\n        # cleanup silently falls back to Chroma and remote chunks\n        # written before the cancel stick around.\n        kb_record = _kb_guard.record or await knowledge_base_service.get_by_user_and_name(\n            _kb_guard.owner_user.id, kb_name\n        )\n        backend_type_value = (\n            kb_record.backend_type if kb_record and kb_record.backend_type else BackendType.CHROMA.value\n        )\n        backend_config = (kb_record.backend_config or {}) if kb_record is not None else {}","sourceCodeStart":2281,"sourceCodeEnd":2317,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/v1/knowledge_bases.py#L2281-L2317","documentation":"400 from POST /api/v1/knowledge_bases/{kb_name}/cancel. A latest job was found, but its status is terminal — 'completed', 'failed', 'cancelled', or 'timed_out' — so there is nothing to cancel. The endpoint only cancels jobs still in a live state (running/pending). The message echoes the actual status so callers can distinguish each case.","triggerScenarios":"Cancelling after ingestion already finished; double-clicking Cancel (second request sees status 'cancelled'); cancelling a job that timed out on its own.","commonSituations":"Race between the UI's Cancel button and a small ingestion finishing first; stale UI showing an in-progress badge for a job that already completed; retry logic re-sending a cancel.","solutions":["Re-fetch the job/run status before cancelling and skip if terminal","Treat 'completed'/'cancelled' responses as success — the end state you wanted is already reached","Debounce/disable the Cancel button once a cancel request is in flight"],"exampleFix":"// before\nawait api.post(`/api/v1/knowledge_bases/${kbName}/cancel`);\n\n// after\nconst jobs = await api.get(`/api/v1/knowledge_bases/${kbName}/runs`);\nconst latest = jobs.data.results?.[0];\nif ([\"completed\", \"failed\", \"cancelled\", \"timed_out\"].includes(latest?.status)) {\n  console.log(`Nothing to cancel, job already ${latest.status}`);\n} else {\n  await api.post(`/api/v1/knowledge_bases/${kbName}/cancel`);\n}","handlingStrategy":"type-guard","validationCode":"TERMINAL = {\"completed\", \"failed\", \"cancelled\", \"timed_out\"}\nlatest = (await client.get(f\"/api/v1/knowledge_bases/{kb_name}/runs\")).json()[\"results\"][0]\nif latest[\"status\"] not in TERMINAL:\n    await client.post(f\"/api/v1/knowledge_bases/{kb_name}/cancel\")","typeGuard":"const TERMINAL = new Set([\"completed\", \"failed\", \"cancelled\", \"timed_out\"]);\nfunction isCancellable(status: string): boolean {\n  return !TERMINAL.has(status);\n}","tryCatchPattern":"try:\n    await client.post(f\"/api/v1/knowledge_bases/{kb_name}/cancel\")\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 400 and \"Cannot cancel\" in e.response.json()[\"detail\"]:\n        return  # already terminal — goal state reached\n    raise","preventionTips":["Check the latest job status immediately before cancelling","Treat 'already cancelled/completed' as success in cancel flows","Disable/debounce the Cancel button while a cancel request is in flight"],"tags":["knowledge-base","ingestion","cancel","http-400","race-condition","langflow"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}