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

dimas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 19e343c3d File IO UnknownHostException 404 (#1159)
19e343c3d is described below

commit 19e343c3de97668c8f0d799e25c596bd6ac5fe7b
Author: Andrew Guterman <[email protected]>
AuthorDate: Wed Mar 12 07:17:07 2025 -0700

    File IO UnknownHostException 404 (#1159)
    
    * File IO exception re-mapping
    
    * ExceptionMappingFileIO in DefaultFileIOFactory
---
 .../exceptions/FileIOUnknownHostException.java     |  29 ++++++
 .../quarkus/catalog/BasePolarisCatalogTest.java    |  35 ++++---
 .../service/catalog/io/DefaultFileIOFactory.java   |   3 +-
 .../service/catalog/io/ExceptionMappingFileIO.java | 111 +++++++++++++++++++++
 .../service/exception/IcebergExceptionMapper.java  |  58 +++++------
 .../catalog/io/ExceptionMappingFileIOTest.java     |  68 +++++++++++++
 .../service/catalog/io/FileIOFactoryTest.java      |   4 +-
 .../exception/IcebergExceptionMapperTest.java      |   9 +-
 8 files changed, 272 insertions(+), 45 deletions(-)

diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/exceptions/FileIOUnknownHostException.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/exceptions/FileIOUnknownHostException.java
new file mode 100644
index 000000000..2e830205a
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/exceptions/FileIOUnknownHostException.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.exceptions;
+
+/**
+ * A {@link PolarisException} implementation for when an UnknownHostException 
happens during File IO
+ * to S3, GCS, or Azure.
+ */
+public class FileIOUnknownHostException extends PolarisException {
+  public FileIOUnknownHostException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
index aef75c2dd..fbf6db504 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/BasePolarisCatalogTest.java
@@ -109,6 +109,7 @@ import org.apache.polaris.service.admin.PolarisAdminService;
 import org.apache.polaris.service.catalog.BasePolarisCatalog;
 import org.apache.polaris.service.catalog.PolarisPassthroughResolutionView;
 import org.apache.polaris.service.catalog.io.DefaultFileIOFactory;
+import org.apache.polaris.service.catalog.io.ExceptionMappingFileIO;
 import org.apache.polaris.service.catalog.io.FileIOFactory;
 import org.apache.polaris.service.catalog.io.MeasuredFileIOFactory;
 import org.apache.polaris.service.config.RealmEntityManagerFactory;
@@ -473,7 +474,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
 
     // Now also check that despite creating the metadata file, the validation 
call still doesn't
     // create any namespaces or tables.
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
     fileIO.addFile(
         tableMetadataLocation,
         
TableMetadataParser.toJson(createSampleTableMetadata(tableLocation)).getBytes(UTF_8));
@@ -608,7 +609,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         tableMetadataLocation,
@@ -652,7 +653,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         tableMetadataLocation,
@@ -801,7 +802,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
                 
PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true")
             .build());
     BasePolarisCatalog catalog = catalog();
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         tableMetadataLocation,
@@ -898,7 +899,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         metadataLocation,
@@ -953,7 +954,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     Namespace namespace = Namespace.of("parent", "child1");
     TableIdentifier table = TableIdentifier.of(namespace, "table");
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     // The location of the metadata JSON file specified in the create will be 
forbidden.
     final String metadataLocation = "http://maliciousdomain.com/metadata.json";;
@@ -1031,7 +1032,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         tableMetadataLocation,
@@ -1083,7 +1084,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         tableMetadataLocation,
@@ -1136,7 +1137,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         tableMetadataLocation,
@@ -1174,7 +1175,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(timestamp);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         tableMetadataLocation,
@@ -1247,7 +1248,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     // Though the metadata JSON file itself is in an allowed location, make it 
internally specify
     // a forbidden table location.
@@ -1325,7 +1326,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         tableMetadataLocation,
@@ -1377,7 +1378,7 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
     update.setTimestamp(230950845L);
     request.setPayload(update);
 
-    InMemoryFileIO fileIO = (InMemoryFileIO) catalog.getIo();
+    InMemoryFileIO fileIO = getInMemoryIo(catalog);
 
     fileIO.addFile(
         tableMetadataLocation,
@@ -1448,7 +1449,9 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
                     metaStoreManagerFactory,
                     configurationStore))
             .apply(taskEntity, callContext);
-    
Assertions.assertThat(fileIO).isNotNull().isInstanceOf(InMemoryFileIO.class);
+    
Assertions.assertThat(fileIO).isNotNull().isInstanceOf(ExceptionMappingFileIO.class);
+    Assertions.assertThat(((ExceptionMappingFileIO) fileIO).getInnerIo())
+        .isInstanceOf(InMemoryFileIO.class);
   }
 
   @Test
@@ -1762,4 +1765,8 @@ public abstract class BasePolarisCatalogTest extends 
CatalogTests<BasePolarisCat
         .isInstanceOf(CommitFailedException.class)
         .hasMessageContaining("conflict_table");
   }
+
+  private static InMemoryFileIO getInMemoryIo(BasePolarisCatalog catalog) {
+    return (InMemoryFileIO) ((ExceptionMappingFileIO) 
catalog.getIo()).getInnerIo();
+  }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/catalog/io/DefaultFileIOFactory.java
 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/DefaultFileIOFactory.java
index 48d9a280a..0a07a6097 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/catalog/io/DefaultFileIOFactory.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/DefaultFileIOFactory.java
@@ -114,6 +114,7 @@ public class DefaultFileIOFactory implements FileIOFactory {
   @VisibleForTesting
   FileIO loadFileIOInternal(
       @Nonnull String ioImplClassName, @Nonnull Map<String, String> 
properties) {
-    return CatalogUtil.loadFileIO(ioImplClassName, properties, new 
Configuration());
+    return new ExceptionMappingFileIO(
+        CatalogUtil.loadFileIO(ioImplClassName, properties, new 
Configuration()));
   }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/catalog/io/ExceptionMappingFileIO.java
 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/ExceptionMappingFileIO.java
new file mode 100644
index 000000000..4ac718079
--- /dev/null
+++ 
b/service/common/src/main/java/org/apache/polaris/service/catalog/io/ExceptionMappingFileIO.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.catalog.io;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.net.UnknownHostException;
+import java.util.Map;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.InputFile;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.polaris.core.exceptions.FileIOUnknownHostException;
+
+/** A {@link FileIO} implementation that wraps an existing FileIO and re-maps 
exceptions */
+public class ExceptionMappingFileIO implements FileIO {
+  private final FileIO io;
+
+  public ExceptionMappingFileIO(FileIO io) {
+    this.io = io;
+  }
+
+  private void handleException(RuntimeException e) {
+    for (Throwable t : ExceptionUtils.getThrowables(e)) {
+      // UnknownHostException isn't a RuntimeException so it's always wrapped
+      if (t instanceof UnknownHostException) {
+        throw new FileIOUnknownHostException("UnknownHostException during File 
IO", t);
+      }
+    }
+  }
+
+  @VisibleForTesting
+  public FileIO getInnerIo() {
+    return io;
+  }
+
+  @Override
+  public InputFile newInputFile(String path) {
+    try {
+      return io.newInputFile(path);
+    } catch (RuntimeException e) {
+      handleException(e);
+      throw e;
+    }
+  }
+
+  @Override
+  public OutputFile newOutputFile(String path) {
+    try {
+      return io.newOutputFile(path);
+    } catch (RuntimeException e) {
+      handleException(e);
+      throw e;
+    }
+  }
+
+  @Override
+  public void deleteFile(String path) {
+    try {
+      io.deleteFile(path);
+    } catch (RuntimeException e) {
+      handleException(e);
+      throw e;
+    }
+  }
+
+  @Override
+  public Map<String, String> properties() {
+    try {
+      return io.properties();
+    } catch (RuntimeException e) {
+      handleException(e);
+      throw e;
+    }
+  }
+
+  @Override
+  public void initialize(Map<String, String> properties) {
+    try {
+      io.initialize(properties);
+    } catch (RuntimeException e) {
+      handleException(e);
+      throw e;
+    }
+  }
+
+  @Override
+  public void close() {
+    try {
+      io.close();
+    } catch (RuntimeException e) {
+      handleException(e);
+      throw e;
+    }
+  }
+}
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
 
b/service/common/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
index c606194c5..81bfbf21f 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
@@ -30,7 +30,6 @@ import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.Response.Status;
 import jakarta.ws.rs.ext.ExceptionMapper;
 import jakarta.ws.rs.ext.Provider;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Locale;
 import java.util.Optional;
@@ -57,6 +56,7 @@ import 
org.apache.iceberg.exceptions.ServiceUnavailableException;
 import org.apache.iceberg.exceptions.UnprocessableEntityException;
 import org.apache.iceberg.exceptions.ValidationException;
 import org.apache.iceberg.rest.responses.ErrorResponse;
+import org.apache.polaris.core.exceptions.FileIOUnknownHostException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.event.Level;
@@ -116,14 +116,6 @@ public class IcebergExceptionMapper implements 
ExceptionMapper<RuntimeException>
     return errorResp;
   }
 
-  /**
-   * @return whether any throwable in the chain case-insensitive-contains the 
given message
-   */
-  static boolean doesAnyThrowableContainAccessDeniedHint(Throwable t) {
-    return Arrays.stream(ExceptionUtils.getThrowables(t))
-        .anyMatch(th -> containsAnyAccessDeniedHint(th.getMessage()));
-  }
-
   public static boolean containsAnyAccessDeniedHint(String message) {
     String messageLower = message.toLowerCase(Locale.ENGLISH);
     return ACCESS_DENIED_HINTS.stream().anyMatch(messageLower::contains);
@@ -165,12 +157,12 @@ public class IcebergExceptionMapper implements 
ExceptionMapper<RuntimeException>
   }
 
   static int mapExceptionToResponseCode(RuntimeException rex) {
-    Optional<Throwable> cloudException =
-        Arrays.stream(ExceptionUtils.getThrowables(rex))
-            .filter(IcebergExceptionMapper::isCloudException)
-            .findAny();
-    if (cloudException.isPresent()) {
-      return mapCloudExceptionToResponseCode(cloudException.get());
+    for (Throwable t : ExceptionUtils.getThrowables(rex)) {
+      // Cloud exceptions can be wrapped by the Iceberg SDK
+      Optional<Integer> code = mapCloudExceptionToResponseCode(t);
+      if (code.isPresent()) {
+        return code.get();
+      }
     }
 
     return switch (rex) {
@@ -179,6 +171,7 @@ public class IcebergExceptionMapper implements 
ExceptionMapper<RuntimeException>
       case NoSuchTableException e -> Status.NOT_FOUND.getStatusCode();
       case NoSuchViewException e -> Status.NOT_FOUND.getStatusCode();
       case NotFoundException e -> Status.NOT_FOUND.getStatusCode();
+      case FileIOUnknownHostException e -> Status.NOT_FOUND.getStatusCode();
       case AlreadyExistsException e -> Status.CONFLICT.getStatusCode();
       case CommitFailedException e -> Status.CONFLICT.getStatusCode();
       case UnprocessableEntityException e -> 422;
@@ -202,10 +195,6 @@ public class IcebergExceptionMapper implements 
ExceptionMapper<RuntimeException>
     };
   }
 
-  private static boolean isCloudException(Throwable t) {
-    return t instanceof S3Exception || t instanceof AzureException || t 
instanceof StorageException;
-  }
-
   /**
    * We typically call cloud providers over HTTP, so when there's an exception 
there's typically an
    * associated HTTP code. This extracts the HTTP code if possible.
@@ -223,9 +212,22 @@ public class IcebergExceptionMapper implements 
ExceptionMapper<RuntimeException>
     };
   }
 
-  static int mapCloudExceptionToResponseCode(Throwable t) {
-    if (doesAnyThrowableContainAccessDeniedHint(t)) {
-      return Status.FORBIDDEN.getStatusCode();
+  /**
+   * Tries mapping a cloud exception to the HTTP code that Polaris should 
return
+   *
+   * @param t the throwable/exception
+   * @return the HTTP code Polaris should return, if it was possible to return 
a suitable mapping.
+   *     Optional.empty() otherwise.
+   */
+  static Optional<Integer> mapCloudExceptionToResponseCode(Throwable t) {
+    if (!(t instanceof S3Exception
+        || t instanceof AzureException
+        || t instanceof StorageException)) {
+      return Optional.empty();
+    }
+
+    if (containsAnyAccessDeniedHint(t.getMessage())) {
+      return Optional.of(Status.FORBIDDEN.getStatusCode());
     }
 
     int httpCode = extractHttpCodeFromCloudException(t);
@@ -233,29 +235,29 @@ public class IcebergExceptionMapper implements 
ExceptionMapper<RuntimeException>
     Status.Family httpFamily = Status.Family.familyOf(httpCode);
 
     if (httpStatus == Status.NOT_FOUND) {
-      return Status.BAD_REQUEST.getStatusCode();
+      return Optional.of(Status.BAD_REQUEST.getStatusCode());
     }
     if (httpStatus == Status.UNAUTHORIZED) {
-      return Status.FORBIDDEN.getStatusCode();
+      return Optional.of(Status.FORBIDDEN.getStatusCode());
     }
     if (httpStatus == Status.BAD_REQUEST
         || httpStatus == Status.FORBIDDEN
         || httpStatus == Status.REQUEST_TIMEOUT
         || httpStatus == Status.TOO_MANY_REQUESTS
         || httpStatus == Status.GATEWAY_TIMEOUT) {
-      return httpCode;
+      return Optional.of(httpCode);
     }
     if (httpFamily == Status.Family.REDIRECTION) {
       // Currently Polaris doesn't know how to follow redirects from cloud 
providers, thus clients
       // shouldn't expect it to.
       // This is a 4xx error to indicate that the client may be able to 
resolve this by changing
       // some data, such as their catalog's region.
-      return 422;
+      return Optional.of(422);
     }
     if (httpFamily == Status.Family.SERVER_ERROR) {
-      return Status.BAD_GATEWAY.getStatusCode();
+      return Optional.of(Status.BAD_GATEWAY.getStatusCode());
     }
 
-    return Status.INTERNAL_SERVER_ERROR.getStatusCode();
+    return Optional.of(Status.INTERNAL_SERVER_ERROR.getStatusCode());
   }
 }
diff --git 
a/service/common/src/test/java/org/apache/polaris/service/catalog/io/ExceptionMappingFileIOTest.java
 
b/service/common/src/test/java/org/apache/polaris/service/catalog/io/ExceptionMappingFileIOTest.java
new file mode 100644
index 000000000..e65c9564b
--- /dev/null
+++ 
b/service/common/src/test/java/org/apache/polaris/service/catalog/io/ExceptionMappingFileIOTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.catalog.io;
+
+import java.net.UnknownHostException;
+import java.util.Map;
+import org.apache.iceberg.io.FileIO;
+import org.apache.polaris.core.exceptions.FileIOUnknownHostException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+/** Unit tests for ExceptionMappingFileIO */
+public class ExceptionMappingFileIOTest {
+  private static final String PATH = "x/y/z";
+  private static final Map<String, String> PROPERTIES = Map.of("k", "v");
+
+  @Test
+  void testProxiesMethodCalls() {
+    try (var io = Mockito.mock(FileIO.class)) {
+      var wrappedIO = new ExceptionMappingFileIO(io);
+
+      wrappedIO.newInputFile(PATH);
+      Mockito.verify(io, Mockito.times(1)).newInputFile(PATH);
+
+      wrappedIO.newOutputFile(PATH);
+      Mockito.verify(io, Mockito.times(1)).newOutputFile(PATH);
+
+      wrappedIO.deleteFile(PATH);
+      Mockito.verify(io, Mockito.times(1)).deleteFile(PATH);
+
+      wrappedIO.properties();
+      Mockito.verify(io, Mockito.times(1)).properties();
+
+      wrappedIO.initialize(PROPERTIES);
+      Mockito.verify(io, Mockito.times(1)).initialize(PROPERTIES);
+
+      wrappedIO.close();
+      Mockito.verify(io, Mockito.times(1)).close();
+    }
+  }
+
+  @Test
+  void testExceptionRemapping() {
+    try (var io = Mockito.mock(FileIO.class)) {
+      Mockito.doThrow(new RuntimeException(new 
UnknownHostException())).when(io).newInputFile(PATH);
+
+      var wrappedIO = new ExceptionMappingFileIO(io);
+      Assertions.assertThrows(FileIOUnknownHostException.class, () -> 
wrappedIO.newInputFile(PATH));
+    }
+  }
+}
diff --git 
a/service/common/src/test/java/org/apache/polaris/service/catalog/io/FileIOFactoryTest.java
 
b/service/common/src/test/java/org/apache/polaris/service/catalog/io/FileIOFactoryTest.java
index 6c808e77e..760185a1d 100644
--- 
a/service/common/src/test/java/org/apache/polaris/service/catalog/io/FileIOFactoryTest.java
+++ 
b/service/common/src/test/java/org/apache/polaris/service/catalog/io/FileIOFactoryTest.java
@@ -190,7 +190,9 @@ public class FileIOFactoryTest {
     TaskEntity taskEntity = TaskEntity.of(tasks.get(0));
     FileIO fileIO =
         new TaskFileIOSupplier(testServices.fileIOFactory()).apply(taskEntity, 
callContext);
-    
Assertions.assertThat(fileIO).isNotNull().isInstanceOf(InMemoryFileIO.class);
+    
Assertions.assertThat(fileIO).isNotNull().isInstanceOf(ExceptionMappingFileIO.class);
+    Assertions.assertThat(((ExceptionMappingFileIO) fileIO).getInnerIo())
+        .isInstanceOf(InMemoryFileIO.class);
 
     // 1. BasePolarisCatalog:doCommit: for writing the table during the 
creation
     // 2. BasePolarisCatalog:doRefresh: for reading the table during the drop
diff --git 
a/service/common/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java
 
b/service/common/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java
index 7cd5737a2..a6fe4113f 100644
--- 
a/service/common/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java
+++ 
b/service/common/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java
@@ -25,9 +25,11 @@ import com.azure.core.exception.HttpResponseException;
 import com.google.cloud.storage.StorageException;
 import jakarta.ws.rs.core.Response;
 import java.io.IOException;
+import java.net.UnknownHostException;
 import java.util.Map;
 import java.util.stream.Stream;
 import org.apache.iceberg.exceptions.RuntimeIOException;
+import org.apache.polaris.core.exceptions.FileIOUnknownHostException;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -62,7 +64,12 @@ public class IcebergExceptionMapperTest {
             Arguments.of(new AzureException("Not Authorized"), 403),
             Arguments.of(new AzureException("Access Denied"), 403),
             Arguments.of(S3Exception.builder().message("Access 
denied").build(), 403),
-            Arguments.of(new StorageException(1, "access denied"), 403)),
+            Arguments.of(new StorageException(1, "access denied"), 403),
+            Arguments.of(
+                new FileIOUnknownHostException(
+                    "mybucket.blob.core.windows.net: Name or service not 
known",
+                    new RuntimeException(new UnknownHostException())),
+                404)),
         cloudCodeMappings.entrySet().stream()
             .flatMap(
                 entry ->

Reply via email to