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 ->