quickwit-oss/quickwit · warning · io::Error (from StorageError)

dynamic message: storage_err.source.to_string() converted in

Error message

dynamic message: storage_err.source.to_string() converted into io::Error (NotFound or Other depending on StorageErrorKind)

What it means

`From<StorageError> for io::Error` maps a storage failure into the standard library error type: NotFound keeps io::ErrorKind::NotFound, every other kind becomes Other, and the error text is just the source error's stringified message. As the in-code TODO notes, the original structured context (kind, source chain) is swallowed, so callers relying on downcasting lose detail.

Source

Thrown at quickwit/quickwit-storage/src/error.rs:87

impl StorageErrorKind {
    /// Creates a StorageError.
    pub fn with_error(self, source: impl Into<anyhow::Error>) -> StorageError {
        StorageError {
            kind: self,
            source: Arc::new(source.into()),
            retry_after: None,
        }
    }
}

impl From<StorageError> for io::Error {
    fn from(storage_err: StorageError) -> Self {
        let io_error_kind = match storage_err.kind() {
            StorageErrorKind::NotFound => io::ErrorKind::NotFound,
            _ => io::ErrorKind::Other,
        };
        // TODO: This is swallowing the context of the source error.
        io::Error::new(io_error_kind, storage_err.source.to_string())
    }
}

/// Generic StorageError.
#[derive(Debug, Clone, Error)]
#[error("storage error(kind={kind:?}, source={source})")]
#[allow(missing_docs)]
pub struct StorageError {
    pub kind: StorageErrorKind,
    #[source]
    source: Arc<anyhow::Error>,
    /// Server-suggested delay before the next retry, if provided (e.g. from `x-amz-retry-after`).
    pub(crate) retry_after: Option<Duration>,
}

/// Generic Result type for storage operations.
pub type StorageResult<T> = Result<T, StorageError>;

View on GitHub (pinned to a39730c5cd)

Solutions

  1. Inspect the io error's Display string to recover the underlying storage message.
  2. Match on the io::ErrorKind only as NotFound vs other — do not expect finer kinds through this conversion.
  3. Preserve the original StorageError in your own error type instead of converting early, so kind() and source() stay available.
  4. If the swallowed context matters for debugging, check storage backend logs/metrics at the time of the failure.

Example fix

// before
let io_err = io::Error::from(storage_err);
// after
let io_err = io::Error::new(io_err.kind(), storage_err); // keep original error where the API permits
// or better: propagate StorageError instead of converting
Defensive patterns

Strategy: try-catch

Type guard

fn is_storage_not_found(err: &std::io::Error) -> bool {
    err.kind() == std::io::ErrorKind::NotFound
}

Try / catch

match operation().await {
    Err(e) if e.kind() == std::io::ErrorKind::NotFound => handle_missing(),
    Err(other) => {
        // kind is Other; the Display string is the only remaining storage detail
        log::warn!("storage failure: {other}");
    }
    Ok(v) => v,
}

Prevention

When it happens

Trigger: Any code path that converts a StorageError into io::Error — e.g. a reader/writer expecting std::io errors receiving a failure from an S3/Azure/GCS/Local storage backend, with the storage kind not being NotFound.

Common situations: Object storage outages, missing credentials/expired tokens surfacing as Other-kind io errors, network failures during split reads, file permissions issues on local storage — all collapsed to a plain message string.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of quickwit-oss/quickwit@a39730c5cd (2026-09-08). Data as JSON: /api/errors/76e97432d296651d. Report an issue: GitHub.