This is an automated email from the ASF dual-hosted git repository. lhotari pushed a commit to branch branch-4.0 in repository https://gitbox.apache.org/repos/asf/pulsar.git
commit cd3535ffb7c2c5e6699a4fd87257e4baf5edebfe Author: Zixuan Liu <[email protected]> AuthorDate: Mon Jan 5 22:18:34 2026 +0800 [fix][admin] Fix asyncGetRequest to handle 204 (#25124) (cherry picked from commit bf98773a2d3f6ecf73560fe4e682ce058dcfde61) --- .../pulsar/client/admin/internal/BaseResource.java | 12 +- .../client/admin/internal/PulsarAdminImpl.java | 6 + .../client/admin/internal/AsyncGetRequestTest.java | 168 +++++++++++++++++++++ 3 files changed, 184 insertions(+), 2 deletions(-) diff --git a/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/BaseResource.java b/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/BaseResource.java index ea39053c2ce..7b00617b10a 100644 --- a/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/BaseResource.java +++ b/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/BaseResource.java @@ -202,11 +202,19 @@ public abstract class BaseResource { new InvocationCallback<Response>() { @Override public void completed(Response response) { - if (response.getStatus() != Response.Status.OK.getStatusCode()) { + int status = response.getStatus(); + // Accept both 200 OK and 204 No Content as success + if (status != Response.Status.OK.getStatusCode() + && status != Response.Status.NO_CONTENT.getStatusCode()) { future.completeExceptionally(getApiException(response)); } else { try { - future.complete(readResponse.apply(response)); + // Handle 204 No Content - no response body to read + if (status == Response.Status.NO_CONTENT.getStatusCode()) { + future.complete(null); + } else { + future.complete(readResponse.apply(response)); + } } catch (Exception e) { future.completeExceptionally(getApiException(e)); } diff --git a/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PulsarAdminImpl.java b/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PulsarAdminImpl.java index aaea8a89f8d..218bdad6afb 100644 --- a/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PulsarAdminImpl.java +++ b/pulsar-client-admin/src/main/java/org/apache/pulsar/client/admin/internal/PulsarAdminImpl.java @@ -19,6 +19,7 @@ package org.apache.pulsar.client.admin.internal; import static com.google.common.base.Preconditions.checkArgument; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.net.URL; import java.util.Map; @@ -441,4 +442,9 @@ public class PulsarAdminImpl implements PulsarAdmin { asyncHttpConnector.close(); } + + @VisibleForTesting + WebTarget getRoot() { + return root; + } } diff --git a/pulsar-client-admin/src/test/java/org/apache/pulsar/client/admin/internal/AsyncGetRequestTest.java b/pulsar-client-admin/src/test/java/org/apache/pulsar/client/admin/internal/AsyncGetRequestTest.java new file mode 100644 index 00000000000..dfcb1d022a4 --- /dev/null +++ b/pulsar-client-admin/src/test/java/org/apache/pulsar/client/admin/internal/AsyncGetRequestTest.java @@ -0,0 +1,168 @@ +/* + * 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.pulsar.client.admin.internal; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import lombok.Cleanup; +import org.apache.pulsar.client.admin.PulsarAdmin; +import org.apache.pulsar.client.admin.PulsarAdminException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class AsyncGetRequestTest { + + WireMockServer server; + String adminUrl; + + @BeforeClass(alwaysRun = true) + void beforeClass() throws IOException { + server = new WireMockServer(WireMockConfiguration.wireMockConfig() + .containerThreads(8) + .port(0)); + server.start(); + adminUrl = "http://localhost:" + server.port(); + } + + @AfterClass(alwaysRun = true) + void afterClass() { + if (server != null) { + server.stop(); + } + } + + @Test + public void testAsyncGetRequest_200OK_WithBody() throws Exception { + // Mock successful response with body + server.stubFor(get(urlEqualTo("/200-with-body")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"name\":\"test-namespace\"}"))); + + @Cleanup + PulsarAdminImpl admin = (PulsarAdminImpl) PulsarAdmin.builder().serviceHttpUrl(adminUrl).build(); + + NamespacesImpl namespaces = (NamespacesImpl) admin.namespaces(); + CompletableFuture<Object> getRequest = + namespaces.asyncGetRequest(admin.getRoot().path("/200-with-body"), Object.class); + + assertThat(getRequest).succeedsWithin(3, TimeUnit.SECONDS); + + // Verify the request was made + server.verify(getRequestedFor(urlEqualTo("/200-with-body"))); + } + + @Test + public void testAsyncGetRequest_204NoContent() throws Exception { + // Mock 204 No Content response + server.stubFor(get(urlEqualTo("/204")) + .willReturn(aResponse() + .withStatus(204))); + + @Cleanup + PulsarAdminImpl admin = (PulsarAdminImpl) PulsarAdmin.builder().serviceHttpUrl(adminUrl).build(); + + NamespacesImpl namespaces = (NamespacesImpl) admin.namespaces(); + CompletableFuture<Object> getRequest = + namespaces.asyncGetRequest(admin.getRoot().path("/204"), Object.class); + + assertThat(getRequest).succeedsWithin(3, TimeUnit.SECONDS); + + // Verify the request was made + server.verify(getRequestedFor(urlEqualTo("/204"))); + } + + @Test + public void testAsyncGetRequest_404NotFound() throws Exception { + // Mock 404 Not Found response + server.stubFor(get(urlEqualTo("/404")) + .willReturn(aResponse() + .withStatus(404) + .withHeader("Content-Type", "application/json") + .withBody("{\"reason\":\"Not found\"}"))); + + @Cleanup + PulsarAdminImpl admin = (PulsarAdminImpl) PulsarAdmin.builder().serviceHttpUrl(adminUrl).build(); + + NamespacesImpl namespaces = (NamespacesImpl) admin.namespaces(); + CompletableFuture<Object> getRequest = + namespaces.asyncGetRequest(admin.getRoot().path("/404"), Object.class); + + assertThat(getRequest) + .failsWithin(3, TimeUnit.SECONDS) + .withThrowableOfType(java.util.concurrent.ExecutionException.class) + .withCauseInstanceOf(PulsarAdminException.class); + + server.verify(getRequestedFor(urlEqualTo("/404"))); + } + + @Test + public void testAsyncGetRequest_500InternalServerError() throws Exception { + // Mock 500 Internal Server Error response + server.stubFor(get(urlEqualTo("/500")) + .willReturn(aResponse() + .withStatus(500) + .withBody("Internal Server Error"))); + + @Cleanup + PulsarAdminImpl admin = (PulsarAdminImpl) PulsarAdmin.builder().serviceHttpUrl(adminUrl).build(); + + NamespacesImpl namespaces = (NamespacesImpl) admin.namespaces(); + CompletableFuture<Object> getRequest = + namespaces.asyncGetRequest(admin.getRoot().path("/500"), Object.class); + + assertThat(getRequest) + .failsWithin(3, TimeUnit.SECONDS) + .withThrowableOfType(java.util.concurrent.ExecutionException.class) + .withCauseInstanceOf(PulsarAdminException.class); + + server.verify(getRequestedFor(urlEqualTo("/500"))); + } + + @Test + public void testAsyncGetRequest_EmptyResponseBody_With200() throws Exception { + // Mock 200 with empty body + server.stubFor(get(urlEqualTo("/200-empty")) + .willReturn(aResponse() + .withStatus(200) + .withBody(""))); + + @Cleanup + PulsarAdminImpl admin = (PulsarAdminImpl) PulsarAdmin.builder().serviceHttpUrl(adminUrl).build(); + + NamespacesImpl namespaces = (NamespacesImpl) admin.namespaces(); + CompletableFuture<Object> getRequest = + namespaces.asyncGetRequest(admin.getRoot().path("/200-empty"), Object.class); + + // Should handle empty body gracefully + assertThat(getRequest).succeedsWithin(3, TimeUnit.SECONDS); + + server.verify(getRequestedFor(urlEqualTo("/200-empty"))); + } +} \ No newline at end of file
