This is an automated email from the ASF dual-hosted git repository.

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 1c7f002e02 fix(arrow-avro): preserve object_store error source in the 
async reader (#10496)
1c7f002e02 is described below

commit 1c7f002e02b69f317993621fc233013760b8413a
Author: ranflarion <[email protected]>
AuthorDate: Mon Aug 31 03:26:17 2026 -0400

    fix(arrow-avro): preserve object_store error source in the async reader 
(#10496)
    
    # Which issue does this PR close?
    
    - Closes #10493.
    
    # Rationale for this change
    
    `AvroObjectReader` converts every error from the underlying
    `ObjectStore` with `AvroError::General(e.to_string())`, and the header
    fetch loop wraps its error the same way, so the typed
    `object_store::Error` is destroyed: `source()` returns nothing and no
    downcast is possible. Callers implementing retry or error-classification
    logic cannot distinguish a permanent `NotFound` from a transient
    transport fault without string matching. `ParquetObjectReader` already
    preserves the source via `E: Into<ParquetError>` in the identical
    `spawn` helper plus `impl From<object_store::Error> for ParquetError`;
    this aligns arrow-avro with that pattern.
    
    # What changes are included in this PR?
    
    - `impl From<object_store::Error> for AvroError` (feature-gated on
    `object_store`), mapping to `AvroError::External(Box::new(e))`, which
    already reports its inner error via `source()` and maps through
    `ArrowError::from_external_error`.
    - The private `spawn` helper bound changes from `E: Error` to `E:
    Into<AvroError>`, mirroring `ParquetObjectReader::spawn`.
    - The header fetch loop propagates the reader error instead of
    reformatting it into `AvroError::General`.
    
    # Are these changes tested?
    
    Two new tests in `store.rs` assert that a read of a missing object
    surfaces an error whose `source()` chain downcasts to
    `object_store::Error::NotFound`, with and without a spawn runtime.
    Existing suites pass unchanged.
    
    # Are there any user-facing changes?
    
    Error `Display` text changes for store failures (`External: ...` instead
    of `Avro error: ...`). Variant selection within the `#[non_exhaustive]`
    `AvroError` is not a stability contract, so this is not considered a
    breaking API change.
    
    ---------
    
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 arrow-avro/src/errors.rs                      |  7 ++++
 arrow-avro/src/reader/async_reader/builder.rs |  9 +----
 arrow-avro/src/reader/async_reader/store.rs   | 57 ++++++++++++++++++++++++---
 3 files changed, 59 insertions(+), 14 deletions(-)

diff --git a/arrow-avro/src/errors.rs b/arrow-avro/src/errors.rs
index dd102abac7..e9a8ca3b87 100644
--- a/arrow-avro/src/errors.rs
+++ b/arrow-avro/src/errors.rs
@@ -135,6 +135,13 @@ impl From<ArrowError> for AvroError {
     }
 }
 
+#[cfg(feature = "object_store")]
+impl From<object_store::Error> for AvroError {
+    fn from(e: object_store::Error) -> AvroError {
+        AvroError::External(Box::new(e))
+    }
+}
+
 impl From<AvroError> for io::Error {
     fn from(e: AvroError) -> Self {
         io::Error::other(e)
diff --git a/arrow-avro/src/reader/async_reader/builder.rs 
b/arrow-avro/src/reader/async_reader/builder.rs
index d3cca70425..064adfad3e 100644
--- a/arrow-avro/src/reader/async_reader/builder.rs
+++ b/arrow-avro/src/reader/async_reader/builder.rs
@@ -154,14 +154,7 @@ where
             break;
         }
 
-        let current_data = reader
-            .get_bytes(range_to_fetch.clone())
-            .await
-            .map_err(|err| {
-                AvroError::General(format!(
-                    "Error fetching Avro header from file reader: {err}"
-                ))
-            })?;
+        let current_data = reader.get_bytes(range_to_fetch.clone()).await?;
         if current_data.is_empty() {
             return Err(AvroError::EOF(
                 "Unexpected EOF while fetching header data".into(),
diff --git a/arrow-avro/src/reader/async_reader/store.rs 
b/arrow-avro/src/reader/async_reader/store.rs
index 44f0b3b42b..d1fdb3e15e 100644
--- a/arrow-avro/src/reader/async_reader/store.rs
+++ b/arrow-avro/src/reader/async_reader/store.rs
@@ -23,7 +23,6 @@ use futures::{FutureExt, TryFutureExt};
 use object_store::ObjectStore;
 use object_store::ObjectStoreExt;
 use object_store::path::Path;
-use std::error::Error;
 use std::ops::Range;
 use std::sync::Arc;
 use tokio::runtime::Handle;
@@ -76,7 +75,7 @@ impl AvroObjectReader {
             + Send
             + 'static,
         O: Send + 'static,
-        E: Error + Send + 'static,
+        E: Into<AvroError> + Send + 'static,
     {
         match &self.runtime {
             Some(handle) => {
@@ -89,13 +88,11 @@ impl AvroObjectReader {
                             Err(e) => Err(AvroError::External(Box::new(e))),
                             Ok(p) => std::panic::resume_unwind(p),
                         },
-                        |res| res.map_err(|e| 
AvroError::General(e.to_string())),
+                        |res| res.map_err(Into::into),
                     )
                     .boxed()
             }
-            None => f(&self.store, &self.path)
-                .map_err(|e| AvroError::General(e.to_string()))
-                .boxed(),
+            None => f(&self.store, &self.path).map_err(Into::into).boxed(),
         }
     }
 }
@@ -116,3 +113,51 @@ impl AsyncFileReader for AvroObjectReader {
         self.spawn(|store, path| async move { store.get_ranges(path, 
&ranges).await }.boxed())
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use object_store::memory::InMemory;
+
+    fn find_object_store_error(err: &AvroError) -> 
Option<&object_store::Error> {
+        let mut source: Option<&(dyn std::error::Error + 'static)> = Some(err);
+        while let Some(e) = source {
+            if let Some(os) = e.downcast_ref::<object_store::Error>() {
+                return Some(os);
+            }
+            source = e.source();
+        }
+        None
+    }
+
+    #[tokio::test]
+    async fn test_get_bytes_preserves_object_store_error_source() {
+        let store = Arc::new(InMemory::new());
+        #[expect(deprecated)]
+        let mut reader = AvroObjectReader::new(store, 
Path::from("missing.avro"));
+        let err = reader.get_bytes(0..10).await.unwrap_err();
+        assert!(
+            matches!(
+                find_object_store_error(&err),
+                Some(object_store::Error::NotFound { .. })
+            ),
+            "expected NotFound in source chain, got: {err}"
+        );
+    }
+
+    #[tokio::test]
+    async fn test_get_bytes_on_runtime_preserves_object_store_error_source() {
+        let store = Arc::new(InMemory::new());
+        #[expect(deprecated)]
+        let mut reader = AvroObjectReader::new(store, 
Path::from("missing.avro"))
+            .with_runtime(Handle::current());
+        let err = reader.get_bytes(0..10).await.unwrap_err();
+        assert!(
+            matches!(
+                find_object_store_error(&err),
+                Some(object_store::Error::NotFound { .. })
+            ),
+            "expected NotFound in source chain, got: {err}"
+        );
+    }
+}

Reply via email to