From 9d70ac7f97b3b3c53c408146d91307d825cc9625 Mon Sep 17 00:00:00 2001 From: shyundev Date: Thu, 6 Aug 2026 20:30:27 +0900 Subject: [PATCH 1/4] Return 503 instead of 500 when the metastore fails during authentication --- CHANGELOG.md | 1 + .../service/auth/DefaultAuthenticator.java | 53 +++++++++++++------ .../auth/DefaultAuthenticatorTest.java | 47 ++++++++++++++++ .../exception/IcebergExceptionMapperTest.java | 5 +- 4 files changed, 90 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bef920fa73..deb26690a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti - Python CLI `setup` now preserves `endpoint_internal` and `sts_endpoint` during apply and export for S3 configuration - Fixed a false-negative in the JDBC optimized location-overlap check (`OPTIMIZED_SIBLING_CHECK`). Ancestor locations stored in `location_without_scheme` without a trailing slash were not matched by the generated ancestor equality terms, allowing nested table/namespace locations to be created under existing prefixes. The query now emits both slash-terminated and non-slash-terminated prefix terms and uses a slash-terminated `LIKE` pattern for descendant matching. - Python CLI `setup` now preserves the Azure `hierarchical` storage flag during apply and export +- A metastore failure while resolving a principal's roles during authentication now returns HTTP 503, as it already did when looking up the principal entity. Previously the failure propagated unwrapped and was reported as HTTP 500, so a backend that served the principal lookup but failed on the grant lookups was reported as a server defect. ### Commits diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java index 65e227b943..570d7b5535 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java @@ -144,12 +144,10 @@ protected PrincipalEntity resolvePrincipalEntity(PolarisCredential credentials) .orElse(null); } } catch (Exception e) { - LOGGER - .atError() - .addKeyValue(StructuredLogKeys.ERR_MSG, e.getMessage()) - .addKeyValue(StructuredLogKeys.STACK_TRACE, Throwables.getStackTraceAsString(e)) - .log("Unable to resolve principal entity from credentials"); - throw new ServiceUnavailableException("Unable to fetch principal entity"); + throw metaStoreUnavailable( + e, + "Unable to resolve principal entity from credentials", + "Unable to fetch principal entity"); } if (principal == null || principal.getType() != PolarisEntityType.PRINCIPAL) { @@ -258,8 +256,13 @@ protected PrincipalRoleSelection extractRequestedRoles(PolarisCredential credent */ protected LoadGrantsResult loadPrincipalGrants(PrincipalEntity principal) { PolarisCallContext polarisContext = callContext.getPolarisCallContext(); - LoadGrantsResult principalGrantResults = - metaStoreManager.loadGrantsToGrantee(polarisContext, principal); + LoadGrantsResult principalGrantResults; + try { + principalGrantResults = metaStoreManager.loadGrantsToGrantee(polarisContext, principal); + } catch (Exception e) { + throw metaStoreUnavailable( + e, "Unable to load grants for principal", "Unable to fetch principal grants"); + } diagnostics.check( principalGrantResults.isSuccess(), "Failed to resolve principal roles for principal name={} id={}", @@ -285,13 +288,33 @@ protected LoadGrantsResult loadPrincipalGrants(PrincipalEntity principal) { if (entitiesById != null) { return entitiesById.get(grant.getSecurableId()); } - return metaStoreManager - .loadEntity( - callContext.getPolarisCallContext(), - grant.getSecurableCatalogId(), - grant.getSecurableId(), - PolarisEntityType.PRINCIPAL_ROLE) - .getEntity(); + PolarisCallContext polarisContext = callContext.getPolarisCallContext(); + try { + return metaStoreManager + .loadEntity( + polarisContext, + grant.getSecurableCatalogId(), + grant.getSecurableId(), + PolarisEntityType.PRINCIPAL_ROLE) + .getEntity(); + } catch (Exception e) { + throw metaStoreUnavailable( + e, "Unable to load securable entity for grant", "Unable to fetch securable entity"); + } + } + + /** + * Logs a metastore failure raised during authentication and returns the exception to throw, so + * that a failing backend is reported as a transient condition instead of an internal error. + */ + private static ServiceUnavailableException metaStoreUnavailable( + Exception cause, String logMessage, String responseMessage) { + LOGGER + .atError() + .addKeyValue(StructuredLogKeys.ERR_MSG, cause.getMessage()) + .addKeyValue(StructuredLogKeys.STACK_TRACE, Throwables.getStackTraceAsString(cause)) + .log(logMessage); + return new ServiceUnavailableException(responseMessage); } protected record PrincipalRoleSelection(Set roles, boolean allRolesRequested) {} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java index d502b73ac0..f91b5d60a6 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java @@ -154,6 +154,53 @@ public void testFetchPrincipalThrowsServiceExceptionOnMetastoreException() { .isInstanceOf(ServiceUnavailableException.class); } + @Test + void testLoadGrantsThrowsServiceExceptionOnMetastoreException() { + // Given: a metastore that fails while loading the principal's grants + PolarisCredential credentials = + PolarisCredential.of( + principalEntity.getId(), null, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); + + PolarisMetaStoreManager metaStoreManagerSpy = Mockito.spy(metaStoreManager); + Mockito.doThrow(new RuntimeException("Metastore exception")) + .when(metaStoreManagerSpy) + .loadGrantsToGrantee(any(), any()); + + DefaultAuthenticator standaloneAuthenticator = newStandaloneAuthenticator(metaStoreManagerSpy); + + // When/Then: the metastore failure should surface as ServiceUnavailableException + assertThatThrownBy(() -> standaloneAuthenticator.authenticate(identityFor(credentials))) + .isInstanceOf(ServiceUnavailableException.class); + } + + @Test + void testLoadSecurableEntityThrowsServiceExceptionOnMetastoreException() { + // Given: grants without preloaded entities, so role resolution falls back to loadEntity, + // and a metastore that fails on that fallback + LoadGrantsResult grants = + metaStoreManager.loadGrantsToGrantee(callContext.getPolarisCallContext(), principalEntity); + LoadGrantsResult grantsWithoutEntities = + new LoadGrantsResult(grants.getGrantsVersion(), grants.getGrantRecords(), null); + + PolarisMetaStoreManager metaStoreManagerSpy = Mockito.spy(metaStoreManager); + Mockito.doReturn(grantsWithoutEntities) + .when(metaStoreManagerSpy) + .loadGrantsToGrantee( + any(), Mockito.argThat(p -> p != null && p.getId() == principalEntity.getId())); + Mockito.doThrow(new RuntimeException("Metastore exception")) + .when(metaStoreManagerSpy) + .loadEntity(any(), anyLong(), anyLong(), Mockito.eq(PolarisEntityType.PRINCIPAL_ROLE)); + + PolarisCredential credentials = + PolarisCredential.of(null, PRINCIPAL_NAME, Set.of(DefaultAuthenticator.PRINCIPAL_ROLE_ALL)); + + DefaultAuthenticator standaloneAuthenticator = newStandaloneAuthenticator(metaStoreManagerSpy); + + // When/Then: the metastore failure should surface as ServiceUnavailableException + assertThatThrownBy(() -> standaloneAuthenticator.authenticate(identityFor(credentials))) + .isInstanceOf(ServiceUnavailableException.class); + } + @Test void testAuthenticationByPrincipalId() { // Given: credentials with principal ID instead of name diff --git a/runtime/service/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java b/runtime/service/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java index 48c51109a6..b1239fdaed 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java @@ -23,6 +23,7 @@ import com.azure.core.exception.AzureException; import com.azure.core.exception.HttpResponseException; import com.google.cloud.storage.StorageException; +import jakarta.ws.rs.ServiceUnavailableException; import jakarta.ws.rs.core.Response; import java.io.IOException; import java.net.UnknownHostException; @@ -118,7 +119,9 @@ static Stream coreExceptionMapping() { Arguments.of(new CommitStateUnknownException(new RuntimeException("db timeout")), 500), Arguments.of(new CommitFailedException("commit failed"), 409), Arguments.of(new AlreadyExistsException("already exists"), 409), - Arguments.of(new ValidationException("invalid"), 400)); + Arguments.of(new ValidationException("invalid"), 400), + // The JAX-RS exception, not Iceberg's; handled by the WebApplicationException case. + Arguments.of(new ServiceUnavailableException("service unavailable"), 503)); } @ParameterizedTest From 704b18c81663437762c6b17afa2dc20f29aa8a25 Mon Sep 17 00:00:00 2001 From: shyundev Date: Sat, 8 Aug 2026 01:08:54 +0900 Subject: [PATCH 2/4] Pass the cause to the log event --- .../org/apache/polaris/service/auth/DefaultAuthenticator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java index 570d7b5535..0f2150dfaf 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java @@ -311,6 +311,7 @@ private static ServiceUnavailableException metaStoreUnavailable( Exception cause, String logMessage, String responseMessage) { LOGGER .atError() + .setCause(cause) .addKeyValue(StructuredLogKeys.ERR_MSG, cause.getMessage()) .addKeyValue(StructuredLogKeys.STACK_TRACE, Throwables.getStackTraceAsString(cause)) .log(logMessage); From eb6c4a7fe0b1dd039b35e812704fccc87a791a38 Mon Sep 17 00:00:00 2001 From: shyundev Date: Fri, 14 Aug 2026 12:55:21 +0900 Subject: [PATCH 3/4] Throw PolarisServiceUnavailableException from metaStoreUnavailable --- CHANGELOG.md | 2 +- .../polaris/service/auth/DefaultAuthenticator.java | 6 +++--- .../service/auth/DefaultAuthenticatorTest.java | 12 ++++++------ .../exception/IcebergExceptionMapperTest.java | 5 +---- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index deb26690a7..987f694592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti - Python CLI `setup` now preserves `endpoint_internal` and `sts_endpoint` during apply and export for S3 configuration - Fixed a false-negative in the JDBC optimized location-overlap check (`OPTIMIZED_SIBLING_CHECK`). Ancestor locations stored in `location_without_scheme` without a trailing slash were not matched by the generated ancestor equality terms, allowing nested table/namespace locations to be created under existing prefixes. The query now emits both slash-terminated and non-slash-terminated prefix terms and uses a slash-terminated `LIKE` pattern for descendant matching. - Python CLI `setup` now preserves the Azure `hierarchical` storage flag during apply and export -- A metastore failure while resolving a principal's roles during authentication now returns HTTP 503, as it already did when looking up the principal entity. Previously the failure propagated unwrapped and was reported as HTTP 500, so a backend that served the principal lookup but failed on the grant lookups was reported as a server defect. +- A metastore failure while resolving a principal's roles during authentication now returns HTTP 503, as it already did when looking up the principal entity; previously it propagated unwrapped and was reported as HTTP 500. All three lookups now report `PolarisServiceUnavailableException` as the error `type`, where the principal entity lookup previously reported `ServiceUnavailableException` with the same status. ### Commits diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java index 0f2150dfaf..9dac6a9375 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java @@ -25,7 +25,6 @@ import io.smallrye.common.annotation.Identifier; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; -import jakarta.ws.rs.ServiceUnavailableException; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -40,6 +39,7 @@ import org.apache.polaris.core.entity.PolarisGrantRecord; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; +import org.apache.polaris.core.exceptions.PolarisServiceUnavailableException; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.LoadGrantsResult; import org.eclipse.microprofile.jwt.JsonWebToken; @@ -307,7 +307,7 @@ protected LoadGrantsResult loadPrincipalGrants(PrincipalEntity principal) { * Logs a metastore failure raised during authentication and returns the exception to throw, so * that a failing backend is reported as a transient condition instead of an internal error. */ - private static ServiceUnavailableException metaStoreUnavailable( + private static PolarisServiceUnavailableException metaStoreUnavailable( Exception cause, String logMessage, String responseMessage) { LOGGER .atError() @@ -315,7 +315,7 @@ private static ServiceUnavailableException metaStoreUnavailable( .addKeyValue(StructuredLogKeys.ERR_MSG, cause.getMessage()) .addKeyValue(StructuredLogKeys.STACK_TRACE, Throwables.getStackTraceAsString(cause)) .log(logMessage); - return new ServiceUnavailableException(responseMessage); + return new PolarisServiceUnavailableException(0, "%s", responseMessage); } protected record PrincipalRoleSelection(Set roles, boolean allRolesRequested) {} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java index f91b5d60a6..ce44085c2e 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java @@ -34,7 +34,6 @@ import io.quarkus.test.junit.QuarkusTest; import io.smallrye.common.annotation.Identifier; import jakarta.inject.Inject; -import jakarta.ws.rs.ServiceUnavailableException; import java.util.Map; import java.util.Set; import org.apache.polaris.core.PolarisDiagnostics; @@ -46,6 +45,7 @@ import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; +import org.apache.polaris.core.exceptions.PolarisServiceUnavailableException; import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.LoadGrantsResult; @@ -151,7 +151,7 @@ public void testFetchPrincipalThrowsServiceExceptionOnMetastoreException() { DefaultAuthenticator standaloneAuthenticator = newStandaloneAuthenticator(metaStoreManagerSpy); assertThatThrownBy(() -> standaloneAuthenticator.authenticate(identityFor(credentials))) - .isInstanceOf(ServiceUnavailableException.class); + .isInstanceOf(PolarisServiceUnavailableException.class); } @Test @@ -168,9 +168,9 @@ void testLoadGrantsThrowsServiceExceptionOnMetastoreException() { DefaultAuthenticator standaloneAuthenticator = newStandaloneAuthenticator(metaStoreManagerSpy); - // When/Then: the metastore failure should surface as ServiceUnavailableException + // When/Then: the metastore failure should surface as PolarisServiceUnavailableException assertThatThrownBy(() -> standaloneAuthenticator.authenticate(identityFor(credentials))) - .isInstanceOf(ServiceUnavailableException.class); + .isInstanceOf(PolarisServiceUnavailableException.class); } @Test @@ -196,9 +196,9 @@ void testLoadSecurableEntityThrowsServiceExceptionOnMetastoreException() { DefaultAuthenticator standaloneAuthenticator = newStandaloneAuthenticator(metaStoreManagerSpy); - // When/Then: the metastore failure should surface as ServiceUnavailableException + // When/Then: the metastore failure should surface as PolarisServiceUnavailableException assertThatThrownBy(() -> standaloneAuthenticator.authenticate(identityFor(credentials))) - .isInstanceOf(ServiceUnavailableException.class); + .isInstanceOf(PolarisServiceUnavailableException.class); } @Test diff --git a/runtime/service/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java b/runtime/service/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java index b1239fdaed..48c51109a6 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/exception/IcebergExceptionMapperTest.java @@ -23,7 +23,6 @@ import com.azure.core.exception.AzureException; import com.azure.core.exception.HttpResponseException; import com.google.cloud.storage.StorageException; -import jakarta.ws.rs.ServiceUnavailableException; import jakarta.ws.rs.core.Response; import java.io.IOException; import java.net.UnknownHostException; @@ -119,9 +118,7 @@ static Stream coreExceptionMapping() { Arguments.of(new CommitStateUnknownException(new RuntimeException("db timeout")), 500), Arguments.of(new CommitFailedException("commit failed"), 409), Arguments.of(new AlreadyExistsException("already exists"), 409), - Arguments.of(new ValidationException("invalid"), 400), - // The JAX-RS exception, not Iceberg's; handled by the WebApplicationException case. - Arguments.of(new ServiceUnavailableException("service unavailable"), 503)); + Arguments.of(new ValidationException("invalid"), 400)); } @ParameterizedTest From 4a3fba7d4b26ef9c985086d5aba53b26b0b63935 Mon Sep 17 00:00:00 2001 From: shyundev Date: Sat, 15 Aug 2026 04:00:39 +0900 Subject: [PATCH 4/4] Treat zero as a valid Retry-After value --- CHANGELOG.md | 2 +- .../service/exception/PolarisExceptionMapper.java | 2 +- .../service/auth/DefaultAuthenticatorTest.java | 12 +++++++++--- .../service/exception/ExceptionMapperTest.java | 13 +++++++++---- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 987f694592..3bba12e819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti - Python CLI `setup` now preserves `endpoint_internal` and `sts_endpoint` during apply and export for S3 configuration - Fixed a false-negative in the JDBC optimized location-overlap check (`OPTIMIZED_SIBLING_CHECK`). Ancestor locations stored in `location_without_scheme` without a trailing slash were not matched by the generated ancestor equality terms, allowing nested table/namespace locations to be created under existing prefixes. The query now emits both slash-terminated and non-slash-terminated prefix terms and uses a slash-terminated `LIKE` pattern for descendant matching. - Python CLI `setup` now preserves the Azure `hierarchical` storage flag during apply and export -- A metastore failure while resolving a principal's roles during authentication now returns HTTP 503, as it already did when looking up the principal entity; previously it propagated unwrapped and was reported as HTTP 500. All three lookups now report `PolarisServiceUnavailableException` as the error `type`, where the principal entity lookup previously reported `ServiceUnavailableException` with the same status. +- A metastore failure while resolving a principal's roles during authentication now returns HTTP 503, as it already did when looking up the principal entity; previously it propagated unwrapped and was reported as HTTP 500. All three lookups now report `PolarisServiceUnavailableException` as the error `type`, where the principal entity lookup previously reported `ServiceUnavailableException` with the same status. All three also send `Retry-After: 0`; the Iceberg REST spec has a client retry a non-idempotent request only when that header is present. ### Commits diff --git a/runtime/service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java b/runtime/service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java index b3ed74bf29..070c8b5dea 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java @@ -57,7 +57,7 @@ public Response toResponse(PolarisException exception) { Response.ResponseBuilder builder = Response.status(statusCode).entity(errorResponse).type(MediaType.APPLICATION_JSON_TYPE); if (exception instanceof PolarisServiceUnavailableException e - && e.getRetryAfterSeconds() != 0) { + && e.getRetryAfterSeconds() >= 0) { builder.header(HttpHeaders.RETRY_AFTER, e.getRetryAfterSeconds()); } return builder.build(); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java index ce44085c2e..14cb2a5f12 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/auth/DefaultAuthenticatorTest.java @@ -151,7 +151,9 @@ public void testFetchPrincipalThrowsServiceExceptionOnMetastoreException() { DefaultAuthenticator standaloneAuthenticator = newStandaloneAuthenticator(metaStoreManagerSpy); assertThatThrownBy(() -> standaloneAuthenticator.authenticate(identityFor(credentials))) - .isInstanceOf(PolarisServiceUnavailableException.class); + .isInstanceOfSatisfying( + PolarisServiceUnavailableException.class, + e -> assertThat(e.getRetryAfterSeconds()).isZero()); } @Test @@ -170,7 +172,9 @@ void testLoadGrantsThrowsServiceExceptionOnMetastoreException() { // When/Then: the metastore failure should surface as PolarisServiceUnavailableException assertThatThrownBy(() -> standaloneAuthenticator.authenticate(identityFor(credentials))) - .isInstanceOf(PolarisServiceUnavailableException.class); + .isInstanceOfSatisfying( + PolarisServiceUnavailableException.class, + e -> assertThat(e.getRetryAfterSeconds()).isZero()); } @Test @@ -198,7 +202,9 @@ void testLoadSecurableEntityThrowsServiceExceptionOnMetastoreException() { // When/Then: the metastore failure should surface as PolarisServiceUnavailableException assertThatThrownBy(() -> standaloneAuthenticator.authenticate(identityFor(credentials))) - .isInstanceOf(PolarisServiceUnavailableException.class); + .isInstanceOfSatisfying( + PolarisServiceUnavailableException.class, + e -> assertThat(e.getRetryAfterSeconds()).isZero()); } @Test diff --git a/runtime/service/src/test/java/org/apache/polaris/service/exception/ExceptionMapperTest.java b/runtime/service/src/test/java/org/apache/polaris/service/exception/ExceptionMapperTest.java index 7ab4284a28..5558a7efd5 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/exception/ExceptionMapperTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/exception/ExceptionMapperTest.java @@ -54,6 +54,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.slf4j.Logger; @@ -112,13 +113,17 @@ public void testNamespaceException() { assertThat(response.getStatus()).isEqualTo(409); } - @Test - public void testServiceUnavailableWithRetryAfter() { + @ParameterizedTest + @CsvSource( + value = {"3, 3", "0, 0", "-1, null"}, + nullValues = "null") + public void testServiceUnavailableRetryAfter(int retryAfterSeconds, String expectedHeader) { PolarisExceptionMapper mapper = new PolarisExceptionMapper(); Response response = - mapper.toResponse(new PolarisServiceUnavailableException(3, "transient conflict")); + mapper.toResponse( + new PolarisServiceUnavailableException(retryAfterSeconds, "transient conflict")); assertThat(response.getStatus()).isEqualTo(503); - assertThat(response.getHeaderString(HttpHeaders.RETRY_AFTER)).isEqualTo("3"); + assertThat(response.getHeaderString(HttpHeaders.RETRY_AFTER)).isEqualTo(expectedHeader); } @ParameterizedTest