{"record":{"id":"74126a2efe311046","repo":"unslothai/unsloth","slug":"failed-to-cancel-export","errorCode":null,"errorMessage":"Failed to cancel export","messagePattern":"Failed to cancel export","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"studio/backend/routes/export.py","lineNumber":166,"sourceCode":"\n\n@router.post(\"/cancel\", response_model = ExportOperationResponse)\nasync def cancel_export(current_subject: str = Depends(get_current_subject)):\n    \"\"\"Cancel the in-flight export by terminating its worker subprocess.\n\n    Only the export subprocess is killed; training and inference run in their\n    own subprocesses and keep going.\n    \"\"\"\n    try:\n        backend = get_export_backend()\n        cancelled = await asyncio.to_thread(backend.cancel_export)\n        return ExportOperationResponse(\n            success = True,\n            message = \"Export cancelled\" if cancelled else \"No active export to cancel\",\n        )\n    except Exception as e:\n        logger.error(f\"Error cancelling export: {e}\", exc_info = True)\n        raise HTTPException(\n            status_code = 500,\n            detail = \"Failed to cancel export\",\n        )\n\n\n@router.get(\"/status\", response_model = ExportStatusResponse)\nasync def get_export_status(current_subject: str = Depends(get_current_subject)):\n    \"\"\"Get export backend status (loaded checkpoint, model type, PEFT flag).\"\"\"\n    try:\n        backend = get_export_backend()\n        last_op = backend.get_last_op()\n        # Relativise the recovered output path the same way the per-op POST response\n        # does, so the success banner shows an identical path on either route.\n        last_op_output_path = None\n        if last_op and last_op.get(\"output_path\"):\n            details = await asyncio.to_thread(_export_details, last_op[\"output_path\"])\n            last_op_output_path = (details or {}).get(\"output_path\")\n        return ExportStatusResponse(","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/export.py#L148-L184","documentation":"Catch-all HTTP 500 when cancel_export raises. Note the design: backend.cancel_export returns a boolean (True if an export was actually killed, False if none was active), and False is a SUCCESS response ('No active export to cancel'); this 500 only fires on a genuine exception during termination.","triggerScenarios":"POST /export/cancel when the backend fails to terminate the export subprocess — e.g. the process handle is stale, the subprocess already became a zombie, or OS-level kill permission errors.","commonSituations":"Cancelling after the worker crashed or was killed externally; PID reuse racing the kill; sandboxed environments restricting process termination.","solutions":["Check the log traceback for 'Error cancelling export'.","Verify via GET /export/status whether an export is actually active before cancelling.","If the subprocess is orphaned, kill it at the OS level (ps + kill) and restart the backend."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const st = await api.get('/export/status');\nif (!st.is_export_active) return; // nothing to cancel; avoids the call entirely","typeGuard":null,"tryCatchPattern":"try {\n  const r = await api.post('/export/cancel');\n  note(r.message); // 'Export cancelled' or 'No active export to cancel'\n} catch (e) {\n  if (e.status === 500) checkServerLogs('Error cancelling export');\n}","preventionTips":["Check is_export_active before cancelling — 'no active export' is a success, not an error.","Handle orphaned subprocesses at the OS level, not by repeat cancels."],"tags":["export","process-management","http-500","cancel"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}