t8y2/dbx · warning · std::io::Error

Excel import consumer closed

Error message

Excel import consumer closed

What it means

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.

Source

Thrown at crates/dbx-core/src/table_import.rs:3093

        .ok_or_else(|| format!("Workbook sheet not found: {sheet_name}"))?;
    let shared_strings_bytes = match zip.by_name("xl/sharedStrings.xml") {
        Ok(file) => file.size(),
        Err(zip::result::ZipError::FileNotFound) => 0,
        Err(error) => return Err(error.to_string()),
    };
    let shared_progress_end = if shared_strings_bytes > 0 { total_bytes / 2 } else { 0 };
    let progress_sender = sender.clone();
    let mut last_shared_progress = Instant::now() - IMPORT_PROGRESS_INTERVAL;
    let mut on_shared_progress = |bytes_read: u64| {
        let progress = bytes_read
            .saturating_mul(shared_progress_end)
            .checked_div(shared_strings_bytes.max(1))
            .unwrap_or_default()
            .min(shared_progress_end);
        if last_shared_progress.elapsed() >= IMPORT_PROGRESS_INTERVAL || bytes_read >= shared_strings_bytes {
            progress_sender
                .blocking_send(Ok(XlsxStreamMessage::Progress(progress)))
                .map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "Excel import consumer closed"))?;
            last_shared_progress = Instant::now();
        }
        Ok(())
    };
    let is_cancelled = || cancelled.load(Ordering::Acquire);
    let mut shared_strings = open_xlsx_shared_strings_with_control(
        &mut zip,
        MAX_IN_MEMORY_XLSX_SHARED_STRINGS_BYTES,
        &is_cancelled,
        &mut on_shared_progress,
    )?;
    let row_range = effective_import_row_range(options)?;
    let sheet = zip.by_name(&sheet_path).map_err(|error| error.to_string())?;
    let uncompressed_sheet_bytes = sheet.size().max(1);
    let mut reader = XmlReader::from_reader(BufReader::new(sheet));
    reader.config_mut().trim_text(false);
    let mut rows = XlsxStreamRowsState::new(sender, row_range, None, expected_columns, batch_size);
    let mut buffer = Vec::new();

View on GitHub (pinned to c0390bff16)

Solutions

  1. Treat BrokenPipe with this message as a normal cancellation/early-close signal, not a data error — stop the import and clean up.
  2. Keep the progress-channel receiver alive until the reader task has finished, or join/await the reader before dropping the consumer.
  3. If closing early is intentional, filter/handle io::ErrorKind::BrokenPipe from the import call and report 'cancelled' instead of 'failed'.
  4. Check for a cancel flag (is_cancelled) being honored promptly so the reader exits itself before the consumer disappears.

Example fix

// before
let mut rx = importer.progress_receiver();
if first_error { drop(rx); } // reader then panics/errors with BrokenPipe
// after
let rx = importer.progress_receiver();
let handle = tokio::spawn(importer.run());
// drain rx until handle completes, then:
drop(rx);
let result = handle.await; // handle BrokenPipe as cancellation if it surfaces
Defensive patterns

Strategy: try-catch

Validate before calling

// Rust: check the receiver is still connected before starting
assert!(!progress_rx.is_closed(), "progress consumer already dropped; aborting import early");

Type guard

fn is_consumer_gone(err: &std::io::Error) -> bool {
    err.kind() == std::io::ErrorKind::BrokenPipe
        && err.to_string().contains("Excel import consumer closed")
}

Try / catch

match table_import::run_xlsx_import(...).await {
    Ok(res) => res,
    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
        // consumer closed: treat as user cancellation, not failure
        ImportOutcome::Cancelled
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/18b044c2f7e29620. Report an issue: GitHub.