{"record":{"id":"18b044c2f7e29620","repo":"t8y2/dbx","slug":"excel-import-consumer-closed","errorCode":null,"errorMessage":"Excel import consumer closed","messagePattern":"Excel import consumer closed","errorType":"error_code","errorClass":"std::io::Error","httpStatus":null,"severity":"warning","filePath":"crates/dbx-core/src/table_import.rs","lineNumber":3093,"sourceCode":"        .ok_or_else(|| format!(\"Workbook sheet not found: {sheet_name}\"))?;\n    let shared_strings_bytes = match zip.by_name(\"xl/sharedStrings.xml\") {\n        Ok(file) => file.size(),\n        Err(zip::result::ZipError::FileNotFound) => 0,\n        Err(error) => return Err(error.to_string()),\n    };\n    let shared_progress_end = if shared_strings_bytes > 0 { total_bytes / 2 } else { 0 };\n    let progress_sender = sender.clone();\n    let mut last_shared_progress = Instant::now() - IMPORT_PROGRESS_INTERVAL;\n    let mut on_shared_progress = |bytes_read: u64| {\n        let progress = bytes_read\n            .saturating_mul(shared_progress_end)\n            .checked_div(shared_strings_bytes.max(1))\n            .unwrap_or_default()\n            .min(shared_progress_end);\n        if last_shared_progress.elapsed() >= IMPORT_PROGRESS_INTERVAL || bytes_read >= shared_strings_bytes {\n            progress_sender\n                .blocking_send(Ok(XlsxStreamMessage::Progress(progress)))\n                .map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, \"Excel import consumer closed\"))?;\n            last_shared_progress = Instant::now();\n        }\n        Ok(())\n    };\n    let is_cancelled = || cancelled.load(Ordering::Acquire);\n    let mut shared_strings = open_xlsx_shared_strings_with_control(\n        &mut zip,\n        MAX_IN_MEMORY_XLSX_SHARED_STRINGS_BYTES,\n        &is_cancelled,\n        &mut on_shared_progress,\n    )?;\n    let row_range = effective_import_row_range(options)?;\n    let sheet = zip.by_name(&sheet_path).map_err(|error| error.to_string())?;\n    let uncompressed_sheet_bytes = sheet.size().max(1);\n    let mut reader = XmlReader::from_reader(BufReader::new(sheet));\n    reader.config_mut().trim_text(false);\n    let mut rows = XlsxStreamRowsState::new(sender, row_range, None, expected_columns, batch_size);\n    let mut buffer = Vec::new();","sourceCodeStart":3075,"sourceCodeEnd":3111,"githubUrl":"https://github.com/t8y2/dbx/blob/c0390bff16418b651f4728520d99adf8ce48829a/crates/dbx-core/src/table_import.rs#L3075-L3111","documentation":"This is not a real io::Error from Excel parsing: when the async consumer of the streaming XLSX import channel has dropped (hung up), blocking_send on the progress channel fails, and the code converts that into a std::io::ErrorKind::BrokenPipe with the message \"Excel import consumer closed\" before propagating it with `?`. The library uses it as an internal signal that nobody is listening on the import stream anymore, so the reader thread should stop.","triggerScenarios":"Calling the streaming XLSX import API (table_import.rs, open_xlsx_shared_strings_with_control path) and dropping/closing the receiver side of the progress channel before the import finishes — e.g. the UI consumer task is cancelled, aborted, or the future holding the receiver is dropped while the reader still emits periodic Progress messages (every IMPORT_PROGRESS_INTERVAL or when shared-strings bytes are fully read).","commonSituations":"User cancels a long Excel import mid-stream; a web request/aborted connection drops the consumer future; a timeout cancels the receiving task while the background reader thread continues; the importer is driven with a channel that is closed early by error handling on the consumer side.","solutions":["Treat BrokenPipe with this message as a normal cancellation/early-close signal, not a data error — stop the import and clean up.","Keep the progress-channel receiver alive until the reader task has finished, or join/await the reader before dropping the consumer.","If closing early is intentional, filter/handle io::ErrorKind::BrokenPipe from the import call and report 'cancelled' instead of 'failed'.","Check for a cancel flag (is_cancelled) being honored promptly so the reader exits itself before the consumer disappears."],"exampleFix":"// before\nlet mut rx = importer.progress_receiver();\nif first_error { drop(rx); } // reader then panics/errors with BrokenPipe\n// after\nlet rx = importer.progress_receiver();\nlet handle = tokio::spawn(importer.run());\n// drain rx until handle completes, then:\ndrop(rx);\nlet result = handle.await; // handle BrokenPipe as cancellation if it surfaces","handlingStrategy":"try-catch","validationCode":"// Rust: check the receiver is still connected before starting\nassert!(!progress_rx.is_closed(), \"progress consumer already dropped; aborting import early\");","typeGuard":"fn is_consumer_gone(err: &std::io::Error) -> bool {\n    err.kind() == std::io::ErrorKind::BrokenPipe\n        && err.to_string().contains(\"Excel import consumer closed\")\n}","tryCatchPattern":"match table_import::run_xlsx_import(...).await {\n    Ok(res) => res,\n    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {\n        // consumer closed: treat as user cancellation, not failure\n        ImportOutcome::Cancelled\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Keep the progress-channel receiver alive until the reader task finishes (join before drop).","Use a cancellation flag checked by the reader instead of dropping the receiver to stop imports.","Map BrokenPipe from import streams to a 'cancelled' status in your UI/error model.","On abort paths, send a final message or close the channel explicitly and await the reader."],"tags":["rust","io","streaming","cancellation","excel-import"],"backgroundTag":"broken-pipe-consumer-closed","analyzedSha":"c0390bff16418b651f4728520d99adf8ce48829a","analyzedAt":"2026-09-05T23:05:10.900Z","contentChangedAt":"2026-09-05T23:05:10.900Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}