From eb7591b25666dd9df479278442b1897a59293267 Mon Sep 17 00:00:00 2001 From: Oguzhan Unlu Date: Tue, 30 Dec 2025 17:41:13 +0300 Subject: [PATCH 1/2] API, Core: Add 404 handling for /v1/config endpoint Add NoSuchWarehouseException and configErrorHandler to handle 404 responses from the /v1/config endpoint, which can occur when a configured warehouse does not exist. Changes: - Add NoSuchWarehouseException in api module - Add configErrorHandler in ErrorHandlers that throws NoSuchWarehouseException on 404 - Update RESTSessionCatalog to use configErrorHandler for config calls - Update OpenAPI spec to document 404 response for /v1/config - Add unit tests for configErrorHandler --- .../exceptions/NoSuchWarehouseException.java | 34 +++++++++++++++++++ .../apache/iceberg/rest/ErrorHandlers.java | 19 +++++++++++ .../iceberg/rest/RESTSessionCatalog.java | 2 +- .../apache/iceberg/rest/TestHTTPClient.java | 22 ++++++++++++ open-api/rest-catalog-open-api.yaml | 18 ++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/org/apache/iceberg/exceptions/NoSuchWarehouseException.java diff --git a/api/src/main/java/org/apache/iceberg/exceptions/NoSuchWarehouseException.java b/api/src/main/java/org/apache/iceberg/exceptions/NoSuchWarehouseException.java new file mode 100644 index 000000000000..94ae50cd1c25 --- /dev/null +++ b/api/src/main/java/org/apache/iceberg/exceptions/NoSuchWarehouseException.java @@ -0,0 +1,34 @@ +/* + * 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.iceberg.exceptions; + +import com.google.errorprone.annotations.FormatMethod; + +/** Exception raised when attempting to load a warehouse that does not exist. */ +public class NoSuchWarehouseException extends RuntimeException { + @FormatMethod + public NoSuchWarehouseException(String message, Object... args) { + super(String.format(message, args)); + } + + @FormatMethod + public NoSuchWarehouseException(Throwable cause, String message, Object... args) { + super(String.format(message, args), cause); + } +} diff --git a/core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java b/core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java index b1575035fcc0..df40fbfcb79e 100644 --- a/core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java +++ b/core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java @@ -30,6 +30,7 @@ import org.apache.iceberg.exceptions.NoSuchPlanTaskException; import org.apache.iceberg.exceptions.NoSuchTableException; import org.apache.iceberg.exceptions.NoSuchViewException; +import org.apache.iceberg.exceptions.NoSuchWarehouseException; import org.apache.iceberg.exceptions.NotAuthorizedException; import org.apache.iceberg.exceptions.RESTException; import org.apache.iceberg.exceptions.ServiceFailureException; @@ -87,6 +88,10 @@ public static Consumer defaultErrorHandler() { return DefaultErrorHandler.INSTANCE; } + public static Consumer configErrorHandler() { + return ConfigErrorHandler.INSTANCE; + } + public static Consumer oauthErrorHandler() { return OAuthErrorHandler.INSTANCE; } @@ -257,6 +262,20 @@ public void accept(ErrorResponse error) { } } + /** Request error handler for config endpoint. */ + private static class ConfigErrorHandler extends DefaultErrorHandler { + private static final ErrorHandler INSTANCE = new ConfigErrorHandler(); + + @Override + public void accept(ErrorResponse error) { + if (error.code() == 404) { + throw new NoSuchWarehouseException("%s", error.message()); + } + + super.accept(error); + } + } + /** * Request error handler that handles the common cases that are included with all responses, such * as 400, 500, etc. diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java index 61e25d3d4fc6..d322598bbbc7 100644 --- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java +++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java @@ -1197,7 +1197,7 @@ private static ConfigResponse fetchConfig( queryParams.build(), ConfigResponse.class, RESTUtil.configHeaders(properties), - ErrorHandlers.defaultErrorHandler()); + ErrorHandlers.configErrorHandler()); configResponse.validate(); return configResponse; } diff --git a/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java b/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java index 8cf97bca32ef..e79bdcf60c81 100644 --- a/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java +++ b/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java @@ -50,6 +50,8 @@ import org.apache.hc.core5.http.HttpStatus; import org.apache.iceberg.IcebergBuild; import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.NoSuchWarehouseException; +import org.apache.iceberg.exceptions.ServiceFailureException; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; import org.apache.iceberg.rest.auth.AuthSession; import org.apache.iceberg.rest.auth.TLSConfigurer; @@ -645,4 +647,24 @@ public boolean equals(Object o) { return Objects.equals(id, item.id) && Objects.equals(data, item.data); } } + + @Test + public void testConfigErrorHandler404ThrowsNoSuchWarehouseException() { + ErrorResponse error = + ErrorResponse.builder().responseCode(404).withMessage("Warehouse not found").build(); + + assertThatThrownBy(() -> ErrorHandlers.configErrorHandler().accept(error)) + .isInstanceOf(NoSuchWarehouseException.class) + .hasMessage("Warehouse not found"); + } + + @Test + public void testConfigErrorHandlerDelegatesToDefaultForNon404() { + ErrorResponse error = + ErrorResponse.builder().responseCode(500).withMessage("Internal server error").build(); + + assertThatThrownBy(() -> ErrorHandlers.configErrorHandler().accept(error)) + .isInstanceOf(ServiceFailureException.class) + .hasMessageContaining("Internal server error"); + } } diff --git a/open-api/rest-catalog-open-api.yaml b/open-api/rest-catalog-open-api.yaml index d322b0c7c7c0..45519ce6d5a5 100644 --- a/open-api/rest-catalog-open-api.yaml +++ b/open-api/rest-catalog-open-api.yaml @@ -162,6 +162,8 @@ paths: $ref: '#/components/responses/UnauthorizedResponse' 403: $ref: '#/components/responses/ForbiddenResponse' + 404: + $ref: '#/components/responses/NoSuchWarehouseResponse' 419: $ref: '#/components/responses/AuthenticationTimeoutResponse' 503: @@ -4660,6 +4662,22 @@ components: } } + # Note that this is a representative example response for use as a shorthand in the spec. + # The fields `message` and `type` as indicated here are not presently prescriptive. + NoSuchWarehouseResponse: + description: Not Found - The given warehouse does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/IcebergErrorResponse' + example: { + "error": { + "message": "The given warehouse does not exist", + "type": "NoSuchWarehouseException", + "code": 404 + } + } + # Note that this is a representative example response for use as a shorthand in the spec. # The fields `message` and `type` as indicated here are not presently prescriptive. UnsupportedOperationResponse: From 10fd3e134b4cdf3ee753136040e340c2d78b6175 Mon Sep 17 00:00:00 2001 From: Oguzhan Unlu Date: Mon, 19 Jan 2026 14:28:26 +0300 Subject: [PATCH 2/2] implement option 1 --- .../org/apache/iceberg/rest/ErrorHandlers.java | 2 +- .../org/apache/iceberg/rest/TestHTTPClient.java | 17 ++++++++++++++++- open-api/rest-catalog-open-api.yaml | 2 -- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java b/core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java index df40fbfcb79e..f059b4cd4af0 100644 --- a/core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java +++ b/core/src/main/java/org/apache/iceberg/rest/ErrorHandlers.java @@ -268,7 +268,7 @@ private static class ConfigErrorHandler extends DefaultErrorHandler { @Override public void accept(ErrorResponse error) { - if (error.code() == 404) { + if (error.code() == 404 && error.type() != null) { throw new NoSuchWarehouseException("%s", error.message()); } diff --git a/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java b/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java index e79bdcf60c81..9856dd9c8e1b 100644 --- a/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java +++ b/core/src/test/java/org/apache/iceberg/rest/TestHTTPClient.java @@ -51,6 +51,7 @@ import org.apache.iceberg.IcebergBuild; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.NoSuchWarehouseException; +import org.apache.iceberg.exceptions.RESTException; import org.apache.iceberg.exceptions.ServiceFailureException; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; import org.apache.iceberg.rest.auth.AuthSession; @@ -651,13 +652,27 @@ public boolean equals(Object o) { @Test public void testConfigErrorHandler404ThrowsNoSuchWarehouseException() { ErrorResponse error = - ErrorResponse.builder().responseCode(404).withMessage("Warehouse not found").build(); + ErrorResponse.builder() + .responseCode(404) + .withType("NotFoundException") + .withMessage("Warehouse not found") + .build(); assertThatThrownBy(() -> ErrorHandlers.configErrorHandler().accept(error)) .isInstanceOf(NoSuchWarehouseException.class) .hasMessage("Warehouse not found"); } + @Test + public void testConfigErrorHandler404ForMisconfiguredUri() { + ErrorResponse error = + ErrorResponse.builder().responseCode(404).withMessage("Not Found").build(); + + assertThatThrownBy(() -> ErrorHandlers.configErrorHandler().accept(error)) + .isInstanceOf(RESTException.class) + .hasMessageContaining("Not Found"); + } + @Test public void testConfigErrorHandlerDelegatesToDefaultForNon404() { ErrorResponse error = diff --git a/open-api/rest-catalog-open-api.yaml b/open-api/rest-catalog-open-api.yaml index 45519ce6d5a5..cdd496a14f21 100644 --- a/open-api/rest-catalog-open-api.yaml +++ b/open-api/rest-catalog-open-api.yaml @@ -4662,8 +4662,6 @@ components: } } - # Note that this is a representative example response for use as a shorthand in the spec. - # The fields `message` and `type` as indicated here are not presently prescriptive. NoSuchWarehouseResponse: description: Not Found - The given warehouse does not exist. content: