{"record":{"id":"af778fa0da71503b","repo":"Zackriya-Solutions/meetily","slug":"copy-task-join-error","errorCode":null,"errorMessage":"Copy task join error: {}","messagePattern":"Copy task join error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src-tauri/src/audio/import.rs","lineNumber":361,"sourceCode":"    let meeting_folder = create_meeting_folder(&base_folder, &title, false)?;\n\n    // Copy audio file to meeting folder\n    emit_progress(&app, \"copying\", 10, \"Copying audio file...\");\n\n    let dest_filename = format!(\n        \"audio.{}\",\n        source\n            .extension()\n            .and_then(|e| e.to_str())\n            .unwrap_or(\"mp4\")\n    );\n    let dest_path = meeting_folder.join(&dest_filename);\n\n    let src = source.clone();\n    let dst = dest_path.clone();\n    tokio::task::spawn_blocking(move || std::fs::copy(&src, &dst))\n        .await\n        .map_err(|e| anyhow!(\"Copy task join error: {}\", e))?\n        .map_err(|e| anyhow!(\"Failed to copy audio file: {}\", e))?;\n\n    info!(\"Copied audio to: {}\", dest_path.display());\n\n    // Check for cancellation\n    if IMPORT_CANCELLED.load(Ordering::SeqCst) {\n        // Cleanup: remove the meeting folder\n        let _ = std::fs::remove_dir_all(&meeting_folder);\n        return Err(anyhow!(\"Import cancelled\"));\n    }\n\n    emit_progress(&app, \"decoding\", 15, \"Decoding audio file...\");\n\n    // Decode the audio file with progress updates\n    let app_for_decode = app.clone();\n    let decode_progress = Box::new(move |progress: u32, msg: &str| {\n        // Map decode progress: 15% + (progress * 0.05) to go from 15% to 20%\n        let overall_progress = 15 + ((progress as f32 * 0.05) as u32);","sourceCodeStart":343,"sourceCodeEnd":379,"githubUrl":"https://github.com/Zackriya-Solutions/meetily/blob/0281737d87d26352fb0adc78c8c0975f691b23d1/frontend/src-tauri/src/audio/import.rs#L343-L379","documentation":"run_import (import.rs:361) spawns the file copy on a blocking thread and the JoinError from tokio::task::spawn_blocking(...).await is wrapped with this message. JoinError means the task never completed normally: it panicked, or it was cancelled at runtime shutdown. The inner copy io::Error has a separate message ('Failed to copy audio file'), so this variant is about the task itself dying, not the filesystem.","triggerScenarios":"std::fs::copy panicking inside the blocking task (a path containing a NUL byte from a mangled IPC string is the classic panic source); the tokio runtime being shut down while the import future still awaits (app quit during import); aborting the task handle externally. Note: the cancellation flag is checked only after the copy, so a user Cancel does NOT produce this error - the copy runs to completion first.","commonSituations":"Closing the Tauri window / quitting mid-copy (runtime drops, spawn_blocking tasks cancelled); a path string crossing IPC with embedded control characters causing fs::copy to panic; very large files over flaky network shares where the app is closed before completion; test harnesses dropping the runtime early.","solutions":["Check the JoinError details: if it is a panic, the panic payload is in the message string - log it and fix the root cause; if cancelled, the app was shutting down, which is expected during quit.","Harden the path: sanitize/reject incoming path strings containing NUL or control characters before spawning the copy.","Do not quit the app mid-import - or accept that shutdown cancels the copy and clean orphan meeting folders on next launch.","For panic resilience, replace fs::copy with an explicit read/write loop returning io::Result instead of panicking on invalid paths.","If this recurs on shutdown, block window close while IMPORT_IN_PROGRESS is set, or move copies to a resilient queue."],"exampleFix":"// before: fs::copy can panic on a NUL-contaminated path, killing the blocking task\ntokio::task::spawn_blocking(move || std::fs::copy(&src, &dst)).await\n    .map_err(|e| anyhow!(\"Copy task join error: {}\", e))?;\n\n// after: validate the path, and copy without panic-on-invalid-input\nif src.to_str().map_or(true, |s| s.contains('\\0')) {\n    return Err(anyhow!(\"Invalid source path\"));\n}\ntokio::task::spawn_blocking(move || std::fs::copy(&src, &dst)).await\n    .map_err(|e| anyhow!(\"Copy task join error: {}\", e))??;","handlingStrategy":"retry","validationCode":"// Rust: reject paths that can panic fs::copy before spawning\nfn copy_is_safe(p: &Path) -> bool {\n    p.to_str().map_or(false, |s| !s.contains('\\0') && !s.contains('\\u{7f}'))\n}\nif !copy_is_safe(&src) { return Err(anyhow!(\"Invalid source path\")); }","typeGuard":null,"tryCatchPattern":"// Retry once on JoinError (transient panic/shutdown race), then surface details\nlet copied = match tokio::task::spawn_blocking(move || std::fs::copy(&src, &dst)).await {\n    Ok(r) => r,\n    Err(join) if join.is_panic() => {\n        warn!(\"copy panicked once: {}\", join);\n        tokio::task::spawn_blocking(move || std::fs::copy(&src2, &dst2)).await\n            .map_err(|e| anyhow!(\"Copy task join error: {}\", e))??\n    }\n    Err(join) => return Err(anyhow!(\"Copy task join error: {}\", join)),\n}?;","preventionTips":["Sanitize IPC-sourced path strings (no NUL/control characters) before spawn_blocking.","Block app quit while an import is running, or accept that shutdown cancels the copy.","Use a chunked read/write copy loop (io::Result) instead of fs::copy to eliminate panic paths.","Clean up orphan meeting folders on next launch in case a cancelled runtime left one."],"tags":["import","tokio","spawn-blocking","join-error","panic","shutdown"],"backgroundTag":"background-task-panic","analyzedSha":"0281737d87d26352fb0adc78c8c0975f691b23d1","analyzedAt":"2026-08-16T20:57:52.567Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}