diff --git a/.github/workflows/e2e-httpclient-tests.yml b/.github/workflows/e2e-httpclient-tests.yml
index c50c3bd395c..7946fa9cbb8 100644
--- a/.github/workflows/e2e-httpclient-tests.yml
+++ b/.github/workflows/e2e-httpclient-tests.yml
@@ -39,7 +39,7 @@ jobs:
fail-fast: false
matrix:
kubernetes: [v1.33.0,v1.32.4,v1.31.8,v1.30.12,v1.29.14]
- httpclient: [jdk,jetty,okhttp]
+ httpclient: [jdk,jetty,okhttp,vertx-5]
steps:
- name: Checkout
uses: actions/checkout@v5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e333e44179..86cdcd06188 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
* Fix #7266: bump jackson-bom from 2.19.2 to 2.20.0, fix overrides and handle jackson-annotations v2.20
#### New Features
+* Fix #7174: Added Vert.x 5 HTTP client implementation with improved async handling and WebSocket separation
#### _**Note**_: Breaking changes
* #7266: bump jackson-bom from 2.19.2 to 2.20.0, fix overrides and handle jackson-annotations v2.20
diff --git a/httpclient-vertx-5/README.md b/httpclient-vertx-5/README.md
new file mode 100644
index 00000000000..a211aebe751
--- /dev/null
+++ b/httpclient-vertx-5/README.md
@@ -0,0 +1,65 @@
+# Vert.x 5 HTTP Client For Fabric8 Kubernetes Client
+
+This module provides Vert.x 5.x HTTP client implementation for the Fabric8 Kubernetes Client, featuring improved async handling and WebSocket separation introduced in Vert.x 5.
+
+## Features
+
+- **Vert.x 5.0.1**: Uses latest stable Vert.x 5.x release
+- **Async Operations**: Enhanced async HTTP request handling
+- **WebSocket Separation**: Vert.x 5's separate HTTP and WebSocket client architecture
+- **Backpressure Support**: Built-in flow control for streaming operations
+- **Stack-based Recursion Guard**: Memory-safe recursion protection without ThreadLocal
+
+## Usage
+
+Add the dependency to your project:
+
+```xml
+
+ io.fabric8
+ kubernetes-httpclient-vertx-5
+ ${fabric8.version}
+
+```
+
+The HTTP client will be automatically discovered via service loader.
+
+## Testing
+
+### Running Integration Tests
+
+To run integration tests specifically with the Vert.x 5 HTTP client:
+
+```bash
+# Run all integration tests with Vert.x 5
+mvn -Phttpclient-vertx-5 -Pitests verify -Dtest.httpclient=vertx-5
+
+# Run specific test with Vert.x 5
+mvn -Phttpclient-vertx-5 test -Dtest=ConfigMapIT -Dtest.httpclient=vertx-5
+
+# Run WebSocket tests with Vert.x 5
+mvn -Phttpclient-vertx-5 test -Dtest=WatchIT -Dtest.httpclient=vertx-5
+```
+
+### Version Validation Test
+
+A special test (`VertxVersionValidationIT`) validates that Vert.x 5.0.1 is actually being used:
+
+```bash
+mvn -Phttpclient-vertx-5 test -Dtest=VertxVersionValidationIT -Dtest.httpclient=vertx-5
+```
+
+### Dependency Verification
+
+Verify the correct Vert.x version is being used:
+
+```bash
+mvn -Phttpclient-vertx-5 dependency:tree | grep vertx
+# Should show: vertx-core:jar:5.0.1:compile
+```
+
+## Architecture Notes
+
+- **WebSocket Client Separation**: Unlike Vert.x 4, Vert.x 5 uses separate HTTP and WebSocket clients
+- **Dependency Management**: This module overrides parent POM's Vert.x version to ensure 5.0.1 is used
+- **Profile Isolation**: The httpclient-vertx-5 profile ensures no conflicts with default Vert.x 4 usage
diff --git a/httpclient-vertx-5/pom.xml b/httpclient-vertx-5/pom.xml
new file mode 100644
index 00000000000..a1f1a55178a
--- /dev/null
+++ b/httpclient-vertx-5/pom.xml
@@ -0,0 +1,235 @@
+
+
+
+ 4.0.0
+
+ kubernetes-client-project
+ io.fabric8
+ 7.5-SNAPSHOT
+
+
+ kubernetes-httpclient-vertx-5
+ jar
+ Fabric8 :: Kubernetes :: HttpClient :: Vert.x 5
+
+
+ 5.0.1
+
+ osgi.extender;
+ filter:="(osgi.extender=osgi.serviceloader.registrar)",
+
+
+ osgi.serviceloader;
+ osgi.serviceloader=io.fabric8.kubernetes.client.http.HttpClient$Factory
+
+
+ !android.util*,
+ *,
+
+
+ io.fabric8.kubernetes.client.vertx*;-noimport:=true,
+
+
+
+
+
+
+
+
+
+ io.vertx
+ vertx-core
+ ${vertx.version}
+
+
+ io.vertx
+ vertx-web-client
+ ${vertx.version}
+
+
+ io.vertx
+ vertx-web-common
+ ${vertx.version}
+
+
+ io.vertx
+ vertx-auth-common
+ ${vertx.version}
+
+
+ io.vertx
+ vertx-core-logging
+ ${vertx.version}
+
+
+ io.vertx
+ vertx-web
+ ${vertx.version}
+
+
+ io.vertx
+ vertx-bridge-common
+ ${vertx.version}
+
+
+ io.vertx
+ vertx-uri-template
+ ${vertx.version}
+
+
+
+
+
+
+ io.fabric8
+ kubernetes-client-api
+
+
+ io.vertx
+ vertx-core
+ ${vertx.version}
+
+
+ io.vertx
+ vertx-web-client
+ ${vertx.version}
+
+
+ io.fabric8
+ kubernetes-client-api
+ test-jar
+ test
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+ org.mockito
+ mockito-inline
+
+
+ io.fabric8
+ mockwebserver
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ ${bouncycastle.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ 1
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+
+ java
+
+
+
+
+ test
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+ report-aggregate
+ verify
+
+ report-aggregate
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ ${maven.bundle.plugin.version}
+
+
+ bundle
+ package
+
+ bundle
+
+
+
+ ${project.name}
+ ${project.groupId}.${project.artifactId}
+ ${osgi.export}
+ ${osgi.import}
+ ${osgi.dynamic.import}
+ ${osgi.require-capability}
+ ${osgi.provide-capability}
+ ${osgi.private}
+ ${osgi.bundles}
+ ${osgi.activator}
+ ${osgi.export.service}
+
+ /META-INF/services/io.fabric8.kubernetes.client.http.HttpClient$Factory=target/classes/META-INF/services/io.fabric8.kubernetes.client.http.HttpClient$Factory,
+
+
+ bundle
+
+
+
+
+
+
+
+
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/AsyncInputReader.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/AsyncInputReader.java
new file mode 100644
index 00000000000..ff608ccb6a9
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/AsyncInputReader.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import lombok.RequiredArgsConstructor;
+
+import java.io.InputStream;
+
+@RequiredArgsConstructor
+class AsyncInputReader {
+
+ private static final int CHUNK_SIZE = 2048;
+
+ private final Vertx vertx;
+ private final InputStream inputStream;
+ private byte[] readBuffer;
+
+ Future readNextChunk() {
+ return vertx.executeBlocking(() -> {
+ if (readBuffer == null) {
+ readBuffer = new byte[CHUNK_SIZE];
+ }
+ final int bytesRead = inputStream.read(readBuffer);
+ if (bytesRead == -1) {
+ return null; // EOF
+ }
+ return Buffer.buffer().appendBytes(readBuffer, 0, bytesRead);
+ });
+ }
+}
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/InputStreamReadStream.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/InputStreamReadStream.java
new file mode 100644
index 00000000000..2548737aa53
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/InputStreamReadStream.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.vertx.core.Handler;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.streams.ReadStream;
+
+import java.io.InputStream;
+
+class InputStreamReadStream implements ReadStream {
+
+ private final VertxHttpRequest vertxHttpRequest;
+ private final HttpClientRequest request;
+ private final AsyncInputReader reader;
+ private final StackBasedRecursionGuard recursionGuard;
+ private final StreamFlowController flowController;
+
+ private Handler exceptionHandler;
+
+ InputStreamReadStream(VertxHttpRequest vertxHttpRequest, InputStream inputStream, HttpClientRequest request) {
+ this.vertxHttpRequest = vertxHttpRequest;
+ this.request = request;
+ this.reader = new AsyncInputReader(vertxHttpRequest.vertx, inputStream);
+ this.recursionGuard = new StackBasedRecursionGuard();
+ this.flowController = new StreamFlowController();
+ }
+
+ @Override
+ public ReadStream exceptionHandler(final Handler handler) {
+ exceptionHandler = handler;
+ return this;
+ }
+
+ @Override
+ public ReadStream handler(final Handler handler) {
+ final boolean shouldStart = !flowController.isInitialized() && handler != null;
+
+ if (shouldStart) {
+ flowController.initialize(vertxHttpRequest.vertx.getOrCreateContext(), this::readChunk);
+ }
+
+ flowController.configureDataHandler(handler);
+
+ if (shouldStart) {
+ readChunk();
+ }
+
+ return this;
+ }
+
+ private void readChunk() {
+ if (recursionGuard.enter()) {
+ try {
+ executeBlockingRead();
+ } finally {
+ recursionGuard.exit();
+ }
+ } else {
+ vertxHttpRequest.vertx.runOnContext(v -> readChunk());
+ }
+ }
+
+ private void executeBlockingRead() {
+ reader.readNextChunk().onComplete(this::handleReadResult);
+ }
+
+ private void handleReadResult(final io.vertx.core.AsyncResult result) {
+ if (result.succeeded()) {
+ final Buffer chunk = result.result();
+ if (chunk != null) {
+ final boolean canContinueWriting = flowController.writeChunk(chunk);
+ if (canContinueWriting) {
+ readChunk();
+ }
+ // If buffer is full, readChunk will be called by drain handler
+ } else {
+ flowController.writeEndSentinel();
+ // No cleanup needed with stack-based approach
+ }
+ } else {
+ handleReadError(result.cause());
+ }
+ }
+
+ private void handleReadError(final Throwable cause) {
+ request.reset(0, cause);
+ if (exceptionHandler != null) {
+ exceptionHandler.handle(cause);
+ }
+ }
+
+ @Override
+ public ReadStream endHandler(Handler handler) {
+ flowController.setEndHandler(handler);
+ return this;
+ }
+
+ @Override
+ public ReadStream pause() {
+ flowController.pause();
+ return this;
+ }
+
+ @Override
+ public ReadStream resume() {
+ flowController.resume();
+ return this;
+ }
+
+ @Override
+ public ReadStream fetch(long amount) {
+ flowController.fetch(amount);
+ return this;
+ }
+}
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/StackBasedRecursionGuard.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/StackBasedRecursionGuard.java
new file mode 100644
index 00000000000..15dab72caef
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/StackBasedRecursionGuard.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+/**
+ * Stack-based recursion guard that prevents stack overflow by tracking call depth.
+ *
+ *
+ * This implementation uses a simple instance variable to track recursion depth,
+ * eliminating ThreadLocal usage and potential memory leaks. Each instance is tied
+ * to a specific component (like InputStreamReadStream) and tracks recursion for
+ * that component only.
+ *
+ *
+ * @since 7.4.0
+ */
+class StackBasedRecursionGuard {
+
+ /**
+ * Maximum recursion depth before requiring async scheduling.
+ * Value preserved from original Vert.x 4 implementation.
+ */
+ private static final int MAX_RECURSION_DEPTH = 8;
+
+ /**
+ * Current recursion depth for this instance.
+ * No synchronization needed as this is tied to single component instance.
+ */
+ private int depth = 0;
+
+ /**
+ * Attempts to enter a recursive call.
+ *
+ * @return {@code true} if recursion is within safe limits and can proceed synchronously,
+ * {@code false} if recursion depth is too high and caller should use async scheduling
+ */
+ boolean enter() {
+ return depth++ < MAX_RECURSION_DEPTH;
+ }
+
+ /**
+ * Exits a recursive call, decreasing the depth counter.
+ * Must be called in a finally block to ensure proper cleanup.
+ */
+ void exit() {
+ depth--;
+ }
+
+ /**
+ * Returns current recursion depth.
+ * Useful for testing and debugging.
+ *
+ * @return current depth count
+ */
+ int getCurrentDepth() {
+ return depth;
+ }
+}
\ No newline at end of file
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/StreamFlowController.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/StreamFlowController.java
new file mode 100644
index 00000000000..363f307b1e1
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/StreamFlowController.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.vertx.core.Context;
+import io.vertx.core.Handler;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.streams.impl.InboundBuffer;
+
+class StreamFlowController {
+
+ private final Buffer endSentinel;
+ private InboundBuffer inboundBuffer;
+ private Handler endHandler;
+
+ StreamFlowController() {
+ this.endSentinel = Buffer.buffer();
+ }
+
+ void initialize(final Context context, final Runnable drainHandler) {
+ inboundBuffer = new InboundBuffer<>(context);
+ inboundBuffer.drainHandler(v -> drainHandler.run());
+ }
+
+ boolean isInitialized() {
+ return inboundBuffer != null;
+ }
+
+ void configureDataHandler(final Handler dataHandler) {
+ if (dataHandler != null) {
+ inboundBuffer.handler(buffer -> {
+ if (buffer == endSentinel) {
+ if (endHandler != null) {
+ endHandler.handle(null);
+ }
+ } else {
+ dataHandler.handle(buffer);
+ }
+ });
+ } else {
+ inboundBuffer.handler(null);
+ }
+ }
+
+ void setEndHandler(final Handler handler) {
+ this.endHandler = handler;
+ }
+
+ boolean writeChunk(final Buffer chunk) {
+ return inboundBuffer.write(chunk);
+ }
+
+ void writeEndSentinel() {
+ inboundBuffer.write(endSentinel);
+ }
+
+ void pause() {
+ if (inboundBuffer != null) {
+ inboundBuffer.pause();
+ }
+ }
+
+ void resume() {
+ if (inboundBuffer != null) {
+ inboundBuffer.resume();
+ }
+ }
+
+ void fetch(final long amount) {
+ if (inboundBuffer != null) {
+ inboundBuffer.fetch(amount);
+ }
+ }
+}
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxAsyncBody.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxAsyncBody.java
new file mode 100644
index 00000000000..ed698246cb3
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxAsyncBody.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AsyncBody;
+import io.vertx.core.http.HttpClientResponse;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Vert.x 5 implementation of {@link AsyncBody} that bridges Vert.x streaming to Fabric8's async body interface.
+ *
+ *
+ * Handles backpressure by starting in a paused state - consumption must be explicitly triggered
+ * via {@link #consume()}. Converts Vert.x buffers to ByteBuffers for the Fabric8 consumer.
+ *
+ *
+ * Must be consumed or cancelled to prevent resource leaks.
+ */
+final class VertxAsyncBody implements AsyncBody {
+ /** The underlying Vert.x response for streaming control. */
+ private final HttpClientResponse resp;
+
+ /** Completion future tracking when streaming is finished or cancelled. */
+ private final CompletableFuture done = new CompletableFuture<>();
+
+ /**
+ * Creates a new VertxAsyncBody bridging Vert.x streaming to Fabric8's AsyncBody interface.
+ * Sets up handlers for data, end, and exception events. Response starts paused.
+ *
+ * @param resp the Vert.x response to stream from
+ * @param consumer the consumer to receive ByteBuffer data
+ */
+ VertxAsyncBody(final HttpClientResponse resp, final AsyncBody.Consumer> consumer) {
+ this.resp = resp;
+
+ resp.handler(buf -> {
+ try {
+ consumer.consume(List.of(ByteBuffer.wrap(buf.getBytes())), this);
+ } catch (Exception e) {
+ resp.request().reset();
+ done.completeExceptionally(e);
+ }
+ }).endHandler(v -> done.complete(null))
+ .exceptionHandler(done::completeExceptionally);
+ }
+
+ /**
+ * Requests the next chunk of data from the Vert.x response.
+ * Implements backpressure by requesting one buffer at a time.
+ */
+ @Override
+ public void consume() {
+ resp.fetch(1);
+ }
+
+ /**
+ * Returns a future that completes when streaming is finished or fails.
+ *
+ * @return completion future
+ */
+ @Override
+ public CompletableFuture done() {
+ return done;
+ }
+
+ /**
+ * Cancels the async body by clearing handlers, resetting the connection,
+ * and cancelling the completion future.
+ */
+ @Override
+ public void cancel() {
+ resp.handler(null);
+ resp.endHandler(null);
+ resp.request().reset();
+ done.cancel(false);
+ }
+}
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClient.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClient.java
new file mode 100644
index 00000000000..a63c6130ade
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClient.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AsyncBody;
+import io.fabric8.kubernetes.client.http.HttpResponse;
+import io.fabric8.kubernetes.client.http.StandardHttpClient;
+import io.fabric8.kubernetes.client.http.StandardHttpRequest;
+import io.fabric8.kubernetes.client.http.StandardWebSocketBuilder;
+import io.fabric8.kubernetes.client.http.WebSocket;
+import io.fabric8.kubernetes.client.http.WebSocketResponse;
+import io.fabric8.kubernetes.client.http.WebSocketUpgradeResponse;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.http.RequestOptions;
+import io.vertx.core.http.UpgradeRejectedException;
+import io.vertx.core.http.WebSocketClient;
+import io.vertx.core.http.WebSocketClientOptions;
+import io.vertx.core.http.WebSocketConnectOptions;
+import lombok.Getter;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static io.fabric8.kubernetes.client.vertx.VertxHttpRequest.toHeadersMap;
+
+public class VertxHttpClient
+ extends StandardHttpClient, F, VertxHttpClientBuilder> {
+
+ private final Vertx vertx;
+ @Getter
+ private final HttpClient httpClient;
+ private final WebSocketClient webSocketClient;
+ private final boolean closeVertx;
+
+ /**
+ * Create a new VertxHttpClient instance using configuration object.
+ * This eliminates constructor ambiguity and provides clear instantiation path.
+ *
+ * @param config configuration containing all required parameters
+ */
+ VertxHttpClient(final VertxHttpClientConfiguration config) {
+ super(config.getClientBuilder(), config.getClosed());
+ this.vertx = config.getClientBuilder().vertx;
+ this.httpClient = config.getHttpClient();
+ this.webSocketClient = createWebSocketClient(config);
+ this.closeVertx = config.isCloseVertx();
+ }
+
+ /**
+ * Creates WebSocket client based on configuration.
+ * Uses custom options if provided, otherwise creates with defaults.
+ */
+ private WebSocketClient createWebSocketClient(final VertxHttpClientConfiguration config) {
+ final WebSocketClientOptions options = config.getWebSocketOptions();
+ return options != null
+ ? vertx.createWebSocketClient(options)
+ : vertx.createWebSocketClient();
+ }
+
+ /**
+ * Creates VertxHttpClient with default WebSocket configuration.
+ * For internal use by VertxHttpClientBuilder.
+ */
+ static VertxHttpClient createWithDefaults(
+ final VertxHttpClientBuilder builder,
+ final AtomicBoolean closed,
+ final HttpClient httpClient,
+ final boolean closeVertx) {
+
+ final VertxHttpClientConfiguration config = VertxHttpClientConfiguration.withDefaultWebSocket(builder, closed,
+ httpClient, closeVertx);
+ return new VertxHttpClient<>(config);
+ }
+
+ /**
+ * Creates VertxHttpClient with custom WebSocket configuration.
+ * For internal use by VertxHttpClientBuilder.
+ */
+ static VertxHttpClient createWithWebSocketOptions(
+ final VertxHttpClientBuilder builder,
+ final AtomicBoolean closed,
+ final HttpClient httpClient,
+ final WebSocketClientOptions wsOptions,
+ final boolean closeVertx) {
+
+ final VertxHttpClientConfiguration config = VertxHttpClientConfiguration.withCustomWebSocket(
+ builder, closed, httpClient, wsOptions, closeVertx);
+ return new VertxHttpClient<>(config);
+ }
+
+ @Override
+ public CompletableFuture buildWebSocketDirect(final StandardWebSocketBuilder standardWebSocketBuilder,
+ WebSocket.Listener listener) {
+ WebSocketConnectOptions options = new WebSocketConnectOptions();
+
+ if (standardWebSocketBuilder.getSubprotocol() != null) {
+ options.setSubProtocols(Collections.singletonList(standardWebSocketBuilder.getSubprotocol()));
+ }
+
+ final StandardHttpRequest request = standardWebSocketBuilder.asHttpRequest();
+
+ if (request.getTimeout() != null) {
+ options.setTimeout(request.getTimeout().toMillis());
+ }
+
+ request.headers().forEach((key, value) -> value.forEach(v -> options.addHeader(key, v)));
+ options.setAbsoluteURI(WebSocket.toWebSocketUri(request.uri()).toString());
+
+ final CompletableFuture response = new CompletableFuture<>();
+
+ webSocketClient
+ .connect(options)
+ .onSuccess(ws -> {
+ final VertxWebSocket ret = new VertxWebSocket(ws, listener);
+ ret.initHandlers();
+ response.complete(new WebSocketResponse(new WebSocketUpgradeResponse(request), ret));
+ }).onFailure(t -> {
+ if (t instanceof UpgradeRejectedException) {
+ final UpgradeRejectedException handshake = (UpgradeRejectedException) t;
+ final WebSocketUpgradeResponse upgradeResponse = new WebSocketUpgradeResponse(
+ request, handshake.getStatus(), toHeadersMap(handshake.getHeaders()));
+ response.complete(new WebSocketResponse(upgradeResponse, handshake));
+ }
+ response.completeExceptionally(t);
+ });
+ return response;
+ }
+
+ @Override
+ public CompletableFuture> consumeBytesDirect(final StandardHttpRequest request,
+ final AsyncBody.Consumer> consumer) {
+
+ final var options = new RequestOptions();
+
+ request.headers().forEach((k, l) -> l.forEach(v -> options.addHeader(k, v)));
+ options.setAbsoluteURI(request.uri().toString());
+ options.setMethod(HttpMethod.valueOf(request.method()));
+
+ if (request.getTimeout() != null) {
+ options.setTimeout(request.getTimeout().toMillis());
+ }
+
+ Optional.ofNullable(request.getContentType())
+ .ifPresent(s -> options.putHeader(HttpHeaders.CONTENT_TYPE, s));
+
+ if (request.isExpectContinue()) {
+ options.putHeader(HttpHeaders.EXPECT, HttpHeaders.CONTINUE);
+ }
+
+ return new VertxHttpRequest(vertx, options, request).consumeBytes(this.httpClient, consumer);
+ }
+
+ @Override
+ public void doClose() {
+ try {
+ httpClient.close();
+ } finally {
+ if (closeVertx) {
+ vertx.close();
+ }
+ }
+ }
+
+}
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilder.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilder.java
new file mode 100644
index 00000000000..1cb454c7df9
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilder.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.KubernetesClientException;
+import io.fabric8.kubernetes.client.http.HttpClient;
+import io.fabric8.kubernetes.client.http.StandardHttpClientBuilder;
+import io.fabric8.kubernetes.client.http.TlsVersion;
+import io.netty.handler.ssl.ApplicationProtocolConfig;
+import io.netty.handler.ssl.IdentityCipherSuiteFilter;
+import io.netty.handler.ssl.JdkSslContext;
+import io.vertx.core.Vertx;
+import io.vertx.core.VertxOptions;
+import io.vertx.core.file.FileSystemOptions;
+import io.vertx.core.http.HttpVersion;
+import io.vertx.core.http.PoolOptions;
+import io.vertx.core.http.WebSocketClientOptions;
+import io.vertx.core.net.JdkSSLEngineOptions;
+import io.vertx.core.net.ProxyOptions;
+import io.vertx.core.net.ProxyType;
+import io.vertx.core.spi.tls.SslContextFactory;
+import io.vertx.ext.web.client.WebClientOptions;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Stream;
+
+import static io.fabric8.kubernetes.client.utils.HttpClientUtils.decodeBasicCredentials;
+import static io.vertx.core.impl.SysProps.DISABLE_DNS_RESOLVER;
+
+/**
+ * Builder for creating Vert.x-based HTTP clients used in Kubernetes API communication.
+ *
+ *
+ * Extends {@link StandardHttpClientBuilder} to provide Vert.x-specific configuration
+ * including connection pooling, SSL/TLS options, proxy settings, and WebSocket support.
+ *
+ * @param the factory type for creating HTTP clients
+ */
+public class VertxHttpClientBuilder
+ extends StandardHttpClientBuilder, F, VertxHttpClientBuilder> {
+
+ private static final int MAX_CONNECTIONS = 8192;
+
+ // the default for etcd seems to be 3 MB, but we'll default to unlimited, so we have the same behavior across clients
+ private static final int MAX_WS_MESSAGE_SIZE = Integer.MAX_VALUE;
+
+ final Vertx vertx;
+ private final boolean closeVertx;
+
+ /**
+ * Creates a builder with optional shared Vert.x instance.
+ * If no shared instance is provided, creates a new one.
+ *
+ * @param clientFactory factory for creating HTTP clients
+ * @param sharedVertx optional shared Vert.x instance
+ */
+ public VertxHttpClientBuilder(final F clientFactory, final Vertx sharedVertx) {
+ this(
+ clientFactory,
+ sharedVertx != null ? sharedVertx : createVertxInstance(),
+ sharedVertx == null);
+ }
+
+ /**
+ * Creates a builder with explicit Vert.x instance and lifecycle management flag.
+ *
+ * @param clientFactory factory for creating HTTP clients
+ * @param vertx Vert.x instance to use
+ * @param closeVertx whether this builder should close the Vert.x instance
+ */
+ VertxHttpClientBuilder(final F clientFactory, final Vertx vertx, final boolean closeVertx) {
+ super(clientFactory);
+ this.vertx = vertx;
+ this.closeVertx = closeVertx;
+ }
+
+ /* ------------------------------------------------------------------------- */
+ /** Assemble connection pool options. */
+ private static PoolOptions createPoolOptions() {
+ return new PoolOptions()
+ .setHttp1MaxSize(MAX_CONNECTIONS)
+ .setHttp2MaxSize(MAX_CONNECTIONS);
+ }
+
+ /** Apply time‑outs, redirects, HTTP/1 preference. */
+ private void applyBasicHttpSettings(final WebClientOptions options) {
+ if (this.connectTimeout != null) {
+ options.setConnectTimeout((int) this.connectTimeout.toMillis());
+ }
+ options.setFollowRedirects(this.followRedirects);
+ if (this.preferHttp11) {
+ options.setProtocolVersion(HttpVersion.HTTP_1_1);
+ }
+ }
+
+ /** Configure proxy and potential auth interceptor. */
+ private void applyProxy(final WebClientOptions options) {
+ if (this.proxyType == HttpClient.ProxyType.DIRECT || this.proxyAddress == null) {
+ return;
+ }
+ final ProxyOptions proxyOptions = new ProxyOptions()
+ .setHost(this.proxyAddress.getHostName())
+ .setPort(this.proxyAddress.getPort())
+ .setType(convertProxyType());
+
+ final String[] userPassword = decodeBasicCredentials(this.proxyAuthorization);
+ if (userPassword != null) {
+ proxyOptions.setUsername(userPassword[0]).setPassword(userPassword[1]);
+ } else {
+ addProxyAuthInterceptor();
+ }
+ options.setProxyOptions(proxyOptions);
+ }
+
+ /** Translate requested TLS versions (if any). */
+ private String[] resolveProtocols() {
+ if (tlsVersions == null || tlsVersions.length == 0) {
+ return new String[0];
+ }
+ return Stream.of(tlsVersions).map(TlsVersion::javaName).toArray(String[]::new);
+ }
+
+ /**
+ * Builds the configured VertxHttpClient instance.
+ *
+ * @return configured VertxHttpClient ready for use
+ */
+ @Override
+ public VertxHttpClient build() {
+ if (this.client != null) {
+ return VertxHttpClient.createWithDefaults(
+ this,
+ this.client.getClosed(),
+ this.client.getHttpClient(),
+ closeVertx);
+ }
+
+ final PoolOptions poolOptions = createPoolOptions();
+ final WebClientOptions httpOptions = new WebClientOptions();
+ applyBasicHttpSettings(httpOptions);
+ applyProxy(httpOptions);
+
+ final String[] protocols = resolveProtocols();
+ if (protocols.length > 0) {
+ httpOptions.setEnabledSecureTransportProtocols(new HashSet<>(Arrays.asList(protocols)));
+ }
+
+ if (this.sslContext != null) {
+ httpOptions.setSsl(true);
+ httpOptions.setSslEngineOptions(createSslEngineOptions(protocols));
+ }
+
+ final WebSocketClientOptions wsOptions = createWebSocketClientOptions(protocols);
+
+ return VertxHttpClient.createWithWebSocketOptions(
+ this,
+ new AtomicBoolean(),
+ vertx.createHttpClient(httpOptions, poolOptions),
+ wsOptions,
+ closeVertx);
+ }
+
+ /** Creates WebSocket client options with appropriate limits for Kubernetes API communication. */
+ private WebSocketClientOptions createWebSocketClientOptions(final String[] protocols) {
+ final WebSocketClientOptions wsOptions = new WebSocketClientOptions();
+
+ wsOptions.setMaxConnections(MAX_CONNECTIONS);
+
+ // the api-server does not seem to fragment messages, so the frames can be very large
+ wsOptions.setMaxFrameSize(MAX_WS_MESSAGE_SIZE);
+ wsOptions.setMaxMessageSize(MAX_WS_MESSAGE_SIZE);
+
+ if (this.sslContext != null) {
+ wsOptions.setSsl(true);
+ wsOptions.setSslEngineOptions(createSslEngineOptions(protocols));
+ }
+
+ return wsOptions;
+ }
+
+ /**
+ * Creates a new builder instance sharing the same Vert.x instance.
+ *
+ * @param clientFactory factory for creating HTTP clients
+ * @return new builder instance
+ */
+ @Override
+ protected VertxHttpClientBuilder newInstance(final F clientFactory) {
+ return new VertxHttpClientBuilder<>(clientFactory, vertx, closeVertx);
+ }
+
+ /** Maps client proxy types to Vert.x proxy types. */
+ private ProxyType convertProxyType() {
+ switch (proxyType) {
+ case HTTP:
+ return ProxyType.HTTP;
+ case SOCKS4:
+ return ProxyType.SOCKS4;
+ case SOCKS5:
+ return ProxyType.SOCKS5;
+ default:
+ throw new KubernetesClientException("Unsupported proxy type");
+ }
+ }
+
+ /** Creates Vert.x instance with DNS resolver disabled to prevent Vault resolution issues. */
+ private static Vertx createVertxInstance() {
+ // We must disable the async DNS resolver as it can cause issues when resolving the Vault instance.
+ // This is done using the DISABLE_DNS_RESOLVER_PROP_NAME system property.
+ // The DNS resolver used by vert.x is configured during the (synchronous) initialization.
+ // So, we just need to disable the async resolver around the Vert.x instance creation.
+ final String originalValue = DISABLE_DNS_RESOLVER.get();
+ Vertx vertx;
+ try {
+ System.setProperty(DISABLE_DNS_RESOLVER.name, "true");
+ vertx = Vertx.vertx(new VertxOptions()
+ .setFileSystemOptions(new FileSystemOptions().setFileCachingEnabled(false).setClassPathResolvingEnabled(false))
+ .setUseDaemonThread(true));
+ } finally {
+ // Restore the original value
+ if (originalValue == null) {
+ System.clearProperty(DISABLE_DNS_RESOLVER.name);
+ } else {
+ System.setProperty(DISABLE_DNS_RESOLVER.name, originalValue);
+ }
+ }
+ return vertx;
+ }
+
+ /** Utility that converts the builder’s {@link #sslContext} + protocols to Vert.x {@link JdkSSLEngineOptions}. */
+ private JdkSSLEngineOptions createSslEngineOptions(final String[] protocols) {
+ return new JdkSSLEngineOptions() {
+ @Override
+ public JdkSSLEngineOptions copy() {
+ return this; // immutable
+ }
+
+ @Override
+ public SslContextFactory sslContextFactory() {
+ return () -> new JdkSslContext(
+ sslContext,
+ true,
+ null,
+ IdentityCipherSuiteFilter.INSTANCE,
+ ApplicationProtocolConfig.DISABLED,
+ io.netty.handler.ssl.ClientAuth.NONE,
+ protocols,
+ false);
+ }
+ };
+ }
+}
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientConfiguration.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientConfiguration.java
new file mode 100644
index 00000000000..6719997d3b8
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientConfiguration.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.http.WebSocketClientOptions;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Configuration object for VertxHttpClient instantiation.
+ *
+ *
+ * Encapsulates all client creation parameters to eliminate constructor ambiguity
+ * that existed between two constructors with similar parameter signatures.
+ * This approach provides a clear and extensible way to configure VertxHttpClient instances.
+ *
+ *
+ *
+ * This class is package-private and used internally by the VertxHttpClient factory methods.
+ * External users should use {@link VertxHttpClientBuilder} instead of creating instances directly.
+ *
+ *
+ * @param the factory type for creating HTTP clients
+ * @since 7.4.0
+ */
+@Builder
+@Getter
+class VertxHttpClientConfiguration {
+
+ @NonNull
+ private final VertxHttpClientBuilder clientBuilder;
+
+ @NonNull
+ private final AtomicBoolean closed;
+
+ @NonNull
+ private final HttpClient httpClient;
+
+ private final WebSocketClientOptions webSocketOptions;
+
+ private final boolean closeVertx;
+
+ /**
+ * Creates a configuration with default WebSocket options.
+ *
+ * @param clientBuilder the builder that created the client
+ * @param closed atomic boolean indicating if client is closed
+ * @param httpClient the Vert.x HTTP client instance
+ * @param closeVertx whether to close Vert.x instance when client closes
+ * @return configuration with default WebSocket settings
+ */
+ static VertxHttpClientConfiguration withDefaultWebSocket(
+ VertxHttpClientBuilder clientBuilder,
+ AtomicBoolean closed,
+ HttpClient httpClient,
+ boolean closeVertx) {
+ return VertxHttpClientConfiguration. builder()
+ .clientBuilder(clientBuilder)
+ .closed(closed)
+ .httpClient(httpClient)
+ .closeVertx(closeVertx)
+ .build();
+ }
+
+ /**
+ * Creates a configuration with custom WebSocket options.
+ *
+ * @param clientBuilder the builder that created the client
+ * @param closed atomic boolean indicating if client is closed
+ * @param httpClient the Vert.x HTTP client instance
+ * @param webSocketOptions custom WebSocket client options
+ * @param closeVertx whether to close Vert.x instance when client closes
+ * @return configuration with custom WebSocket settings
+ */
+ static VertxHttpClientConfiguration withCustomWebSocket(
+ VertxHttpClientBuilder clientBuilder,
+ AtomicBoolean closed,
+ HttpClient httpClient,
+ WebSocketClientOptions webSocketOptions,
+ boolean closeVertx) {
+ return VertxHttpClientConfiguration. builder()
+ .clientBuilder(clientBuilder)
+ .closed(closed)
+ .httpClient(httpClient)
+ .webSocketOptions(webSocketOptions)
+ .closeVertx(closeVertx)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientFactory.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientFactory.java
new file mode 100644
index 00000000000..20b111b7d3e
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientFactory.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.HttpClient;
+import io.vertx.core.Vertx;
+
+/**
+ * Vert.x implementation of {@link io.fabric8.kubernetes.client.http.HttpClient.Factory}.
+ *
+ *
+ * When constructed with a non‑null {@link Vertx} the same instance is reused for every
+ * client; otherwise each {@link #newBuilder()} call will lazily create (and own) its own
+ * Vert.x runtime.
+ *
+ */
+public class VertxHttpClientFactory implements HttpClient.Factory {
+
+ final Vertx vertx;
+
+ /**
+ * Return a factory that reuses the supplied Vert.x instance.
+ */
+ public VertxHttpClientFactory() {
+ this(null);
+ }
+
+ /**
+ * Create a new instance of the factory that will reuse the provided {@link Vertx} instance.
+ *
+ * It's the user's responsibility to manage the lifecycle of the provided Vert.x instance.
+ * Operations such as close, and so on are left on hands of the user.
+ *
+ * @param vertx the Vertx instance to use.
+ */
+ public VertxHttpClientFactory(final Vertx vertx) {
+ this.vertx = vertx;
+ }
+
+ /**
+ * {@inheritDoc} – reuses the shared Vert.x when present, otherwise defers creation to the
+ * builder.
+ */
+ @Override
+ public VertxHttpClientBuilder newBuilder() {
+ return new VertxHttpClientBuilder<>(this, vertx);
+ }
+}
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpRequest.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpRequest.java
new file mode 100644
index 00000000000..112d019f3ba
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpRequest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AsyncBody;
+import io.fabric8.kubernetes.client.http.HttpResponse;
+import io.fabric8.kubernetes.client.http.StandardHttpRequest;
+import io.fabric8.kubernetes.client.http.StandardHttpRequest.BodyContent;
+import io.vertx.core.Future;
+import io.vertx.core.MultiMap;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import io.vertx.core.http.RequestOptions;
+import io.vertx.core.streams.ReadStream;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Vert.x HTTP request implementation that bridges Fabric8's StandardHttpRequest to Vert.x HttpClient.
+ * Handles various body content types and provides async response processing with back-pressure control.
+ */
+class VertxHttpRequest {
+
+ /** Vert.x instance for async operations */
+ final Vertx vertx;
+ /** HTTP request options including URL, method, headers */
+ private final RequestOptions options;
+ /** Source request containing body and metadata */
+ private final StandardHttpRequest request;
+
+ /**
+ * Creates a new Vert.x HTTP request wrapper.
+ *
+ * @param vertx Vert.x instance for async operations
+ * @param options HTTP request options (URL, method, headers)
+ * @param request source request with body and metadata
+ */
+ public VertxHttpRequest(Vertx vertx, RequestOptions options, StandardHttpRequest request) {
+ this.vertx = vertx;
+ this.options = options;
+ this.request = request;
+ }
+
+ /**
+ * Executes the HTTP request asynchronously and returns a response with streaming body support.
+ *
+ * @param client HTTP client to use for the request
+ * @param consumer consumer for processing response body chunks
+ * @return CompletableFuture containing the HTTP response with async body
+ */
+ public CompletableFuture> consumeBytes(HttpClient client,
+ AsyncBody.Consumer> consumer) {
+ return client.request(options)
+ .compose(req -> {
+ // If the caller asked for 100‑continue semantics we first flush the headers,
+ // wait for the server to acknowledge, then stream the body.
+ if (request.isExpectContinue()) {
+ io.vertx.core.Promise promise = io.vertx.core.Promise.promise();
+
+ // Vert.x will invoke this handler when the server replies with 100-Continue
+ req.continueHandler(v -> sendBody(req, request.body()).onComplete(promise));
+
+ // Send just the headers – body will follow from the handler above
+ req.sendHead().onFailure(promise::fail);
+
+ return promise.future();
+ }
+
+ // Normal request – send headers and body in one go
+ return sendBody(req, request.body());
+ })
+ .map(resp -> toFabricResponse(resp, consumer))
+ .toCompletionStage()
+ .toCompletableFuture();
+ }
+
+ /**
+ * Sends the HTTP request with the appropriate body content type.
+ * Supports string, byte array, and input stream body content.
+ *
+ * @param req the Vert.x HTTP client request
+ * @param body the body content to send, or null for no body
+ * @return Future containing the HTTP response
+ */
+ private Future sendBody(HttpClientRequest req, BodyContent body) {
+ if (body == null) {
+ return req.send();
+ }
+
+ if (body instanceof StandardHttpRequest.StringBodyContent) {
+ StandardHttpRequest.StringBodyContent s = (StandardHttpRequest.StringBodyContent) body;
+ return req.send(Buffer.buffer(s.getContent()));
+ }
+ if (body instanceof StandardHttpRequest.ByteArrayBodyContent) {
+ StandardHttpRequest.ByteArrayBodyContent b = (StandardHttpRequest.ByteArrayBodyContent) body;
+ return req.send(Buffer.buffer(b.getContent()));
+ }
+ if (body instanceof StandardHttpRequest.InputStreamBodyContent) {
+ StandardHttpRequest.InputStreamBodyContent i = (StandardHttpRequest.InputStreamBodyContent) body;
+ InputStream is = i.getContent();
+ ReadStream stream = new InputStreamReadStream(this, is, req);
+ return req.send(stream);
+ }
+ return Future.failedFuture(new IllegalArgumentException("Unsupported body content: " + body.getClass()));
+ }
+
+ /**
+ * Converts a Vert.x HTTP response to Fabric8's HttpResponse with async body support.
+ * Pauses the response stream initially to allow back-pressure control from the AsyncBody.
+ *
+ * @param resp the Vert.x HTTP response
+ * @param consumer consumer for processing response body chunks
+ * @return HttpResponse with async body bridge
+ */
+ private HttpResponse toFabricResponse(HttpClientResponse resp,
+ AsyncBody.Consumer> consumer) {
+
+ resp.pause(); // we drive back‑pressure from the AsyncBody
+ VertxAsyncBody asyncBody = new VertxAsyncBody(resp, consumer);
+ return new VertxHttpResponse(asyncBody, resp, request);
+ }
+
+ /**
+ * Converts Vert.x MultiMap headers to standard Map> format.
+ * Preserves multiple values for the same header key.
+ *
+ * @param multiMap Vert.x MultiMap containing headers
+ * @return Map with header names as keys and value lists
+ */
+ static Map> toHeadersMap(MultiMap multiMap) {
+ Map> headers = new LinkedHashMap<>();
+ multiMap.names().forEach(k -> headers.put(k, multiMap.getAll(k)));
+ return headers;
+ }
+}
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpResponse.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpResponse.java
new file mode 100644
index 00000000000..802b182ab2c
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxHttpResponse.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AsyncBody;
+import io.fabric8.kubernetes.client.http.HttpRequest;
+import io.fabric8.kubernetes.client.http.HttpResponse;
+import io.fabric8.kubernetes.client.http.StandardHttpHeaders;
+import io.vertx.core.http.HttpClientResponse;
+
+import java.util.Optional;
+
+/**
+ * Vert.x 5 implementation of {@link HttpResponse} for the Fabric8 Kubernetes Client.
+ *
+ *
+ * Bridges Vert.x 5's {@link HttpClientResponse} with Fabric8's HTTP abstraction,
+ * providing case-insensitive headers and async body streaming for Kubernetes operations
+ * like pod logs, exec sessions, and watch operations.
+ *
+ *
+ * Thread-safe and immutable once constructed. The {@link AsyncBody} must be
+ * consumed or cancelled to prevent resource leaks.
+ *
+ * @since 6.0.0
+ */
+public class VertxHttpResponse extends StandardHttpHeaders implements HttpResponse {
+ /** The async body for streaming consumption. Must be consumed or cancelled. */
+ private final AsyncBody result;
+
+ /** The underlying Vert.x response providing status code and headers. */
+ private final HttpClientResponse resp;
+
+ /** The original request for correlation and debugging. */
+ private final HttpRequest request;
+
+ /**
+ * Constructs a VertxHttpResponse bridging Vert.x 5 and Fabric8 client APIs.
+ * Headers are converted to case-insensitive format.
+ *
+ * @param result the async body, must be consumed or cancelled
+ * @param resp the underlying Vert.x response
+ * @param request the original request for correlation
+ */
+ VertxHttpResponse(AsyncBody result, HttpClientResponse resp, HttpRequest request) {
+ super(VertxHttpRequest.toHeadersMap(resp.headers()));
+ this.result = result;
+ this.resp = resp;
+ this.request = request;
+ }
+
+ /**
+ * Returns the HTTP status code from the underlying Vert.x response.
+ *
+ * @return the HTTP status code (e.g., 200, 404, 500)
+ */
+ @Override
+ public int code() {
+ return resp.statusCode();
+ }
+
+ /**
+ * Returns the async response body for streaming consumption.
+ *
+ *
+ * Provides non-blocking access with backpressure support, ideal for
+ * streaming Kubernetes operations like pod logs, exec sessions, and watch operations.
+ * The body must be consumed or cancelled to prevent resource leaks.
+ *
+ * @return the async body for streaming data consumption
+ */
+ @Override
+ public AsyncBody body() {
+ return result;
+ }
+
+ /**
+ * Returns the original HTTP request that initiated this response.
+ * Useful for debugging, correlation, and retry logic.
+ *
+ * @return the original HTTP request
+ */
+ @Override
+ public HttpRequest request() {
+ return request;
+ }
+
+ /**
+ * Returns the previous response in an HTTP redirect chain.
+ *
+ *
+ * Currently always returns empty as redirect chain tracking is not implemented.
+ * Redirects are rare in Kubernetes API contexts.
+ *
+ * @return empty Optional
+ */
+ @Override
+ public Optional> previousResponse() {
+ return Optional.empty();
+ }
+}
diff --git a/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxWebSocket.java b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxWebSocket.java
new file mode 100644
index 00000000000..ee5efc7fff6
--- /dev/null
+++ b/httpclient-vertx-5/src/main/java/io/fabric8/kubernetes/client/vertx/VertxWebSocket.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.WebSocket;
+import io.netty.handler.codec.http.websocketx.CorruptedWebSocketFrameException;
+import io.vertx.core.Future;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClosedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Vert.x 5 WebSocket adapter implementing Fabric8's {@link WebSocket} SPI with backpressure support.
+ *
+ *
+ * This class bridges Vert.x's native WebSocket implementation to Fabric8's WebSocket abstraction,
+ * enabling Kubernetes WebSocket operations like exec, port-forward, logs streaming, and watch APIs.
+ *
+ *
Thread Safety
+ *
+ * Thread-safe for concurrent access. All Vert.x callbacks execute on the event-loop thread,
+ * while public methods may be called from any thread. Internal state is protected using atomic operations.
+ *
+ *
Backpressure Protocol
+ *
+ * Implements flow control to prevent overwhelming the consumer:
+ *
+ *
WebSocket pauses after each message delivery
+ *
Consumer must call {@link #request()} to receive the next frame
+ *
Violating this protocol will cause the connection to stall
+ *
+ *
+ *
Error Handling
+ *
+ * Translates Vert.x-specific exceptions to standard Java types:
+ *
+ */
+class VertxWebSocket implements WebSocket {
+
+ private static final Logger LOG = LoggerFactory.getLogger(VertxWebSocket.class);
+
+ /** The underlying Vert.x WebSocket providing the network transport */
+ private final io.vertx.core.http.WebSocket webSocket;
+
+ /** Atomic counter tracking bytes pending transmission for backpressure control */
+ private final AtomicInteger pendingBytesCount = new AtomicInteger();
+
+ /** Callback handler for WebSocket lifecycle events and message delivery */
+ private final Listener eventListener;
+
+ /**
+ * Creates a new Vert.x WebSocket adapter.
+ *
+ * @param webSocket the underlying Vert.x WebSocket connection
+ * @param eventListener callback handler for WebSocket events and messages
+ */
+ VertxWebSocket(final io.vertx.core.http.WebSocket webSocket, final Listener eventListener) {
+ this.webSocket = webSocket;
+ this.eventListener = eventListener;
+ }
+
+ /**
+ * Initializes WebSocket handlers and notifies the listener that the connection is ready.
+ * Called by the owning HttpClient once the underlying Vert.x WebSocket is established.
+ */
+ void initHandlers() {
+ setupEventHandlers();
+ eventListener.onOpen(this);
+ }
+
+ /**
+ * Registers Vert.x event handlers and translates them to Fabric8 WebSocket callbacks.
+ * Implements backpressure by pausing after each message until {@link #request()} is called.
+ */
+ private void setupEventHandlers() {
+ // Handle binary messages (e.g., exec terminal data)
+ webSocket.binaryMessageHandler(buffer -> {
+ webSocket.pause();
+ eventListener.onMessage(this, ByteBuffer.wrap(buffer.getBytes()));
+ });
+
+ // Handle text messages (e.g., JSON watch events, logs)
+ webSocket.textMessageHandler(text -> {
+ webSocket.pause();
+ eventListener.onMessage(this, text);
+ });
+
+ // Handle connection close (use end handler for proper frame ordering)
+ webSocket.endHandler(unused -> eventListener.onClose(this, webSocket.closeStatusCode(), webSocket.closeReason()));
+
+ // Handle connection errors with proper cleanup
+ webSocket.exceptionHandler(error -> {
+ try {
+ eventListener.onError(this, mapVertxErrorToStandardException(error));
+ } finally {
+ // Ensure connection is closed after error notification
+ if (!webSocket.isClosed()) {
+ webSocket.close();
+ }
+ }
+ });
+ }
+
+ /**
+ * Maps Vert.x-specific exceptions to standard Java exception types expected by Fabric8 SPI.
+ *
+ * @param vertxError the original Vert.x exception
+ * @return a standard Java exception suitable for the WebSocket SPI
+ */
+ private static Throwable mapVertxErrorToStandardException(final Throwable vertxError) {
+ if (vertxError instanceof CorruptedWebSocketFrameException) {
+ return new ProtocolException(vertxError.getMessage()).initCause(vertxError);
+ }
+ if (vertxError instanceof HttpClosedException) {
+ return new IOException("WebSocket connection closed unexpectedly", vertxError);
+ }
+ return vertxError;
+ }
+
+ /**
+ * Logs failures from asynchronous WebSocket operations that cannot be propagated to the caller.
+ *
+ * @param asyncOperation the asynchronous operation to monitor for failures
+ */
+ private void logAsyncOperationFailures(final Future> asyncOperation) {
+ asyncOperation.onFailure(error -> LOG.error("Asynchronous WebSocket operation failed", error));
+ }
+
+ /**
+ * Sends binary data over the WebSocket connection.
+ *
+ *
+ * Optimizes for zero-copy when possible by using array-backed ByteBuffers directly.
+ * For direct ByteBuffers, copies data to avoid blocking the event loop.
+ *
+ *
+ * Updates the pending bytes counter for backpressure monitoring and supports
+ * both synchronous and asynchronous transmission completion.
+ *
+ * @param buffer the binary data to send; position will be advanced to limit
+ * @return true if the message was queued successfully, false if immediate failure occurred
+ */
+ @Override
+ public boolean send(final ByteBuffer buffer) {
+ final Buffer vertxBuffer = convertToVertxBuffer(buffer);
+ final int messageLength = vertxBuffer.length();
+
+ // Track pending bytes for backpressure
+ pendingBytesCount.addAndGet(messageLength);
+
+ final Future writeOperation = webSocket.writeBinaryMessage(vertxBuffer);
+
+ // Handle immediate failure
+ if (writeOperation.failed()) {
+ pendingBytesCount.addAndGet(-messageLength);
+ LOG.error("WebSocket binary message send failed immediately", writeOperation.cause());
+ return false;
+ }
+
+ // Handle synchronous success
+ if (writeOperation.isComplete()) {
+ pendingBytesCount.addAndGet(-messageLength);
+ return true;
+ }
+
+ // Handle asynchronous completion
+ writeOperation.onComplete(result -> {
+ if (result.failed()) {
+ LOG.error("WebSocket binary message send failed asynchronously", result.cause());
+ }
+ pendingBytesCount.addAndGet(-messageLength);
+ });
+
+ return true;
+ }
+
+ /**
+ * Converts a Java ByteBuffer to a Vert.x Buffer, optimizing for zero-copy when possible.
+ *
+ * @param buffer the source ByteBuffer; position will be advanced to limit
+ * @return a Vert.x Buffer containing the data
+ */
+ private Buffer convertToVertxBuffer(final ByteBuffer buffer) {
+ if (buffer.hasArray()) {
+ // Zero-copy optimization for array-backed buffers
+ final Buffer vertxBuffer = Buffer.buffer(buffer.remaining()).setBytes(0, buffer);
+ buffer.position(buffer.limit());
+ return vertxBuffer;
+ } else {
+ // Copy for direct buffers to avoid blocking event loop
+ final Buffer vertxBuffer = Buffer.buffer(buffer.remaining());
+ final byte[] tempArray = new byte[buffer.remaining()];
+ buffer.get(tempArray);
+ return vertxBuffer.appendBytes(tempArray);
+ }
+ }
+
+ /**
+ * Initiates a graceful WebSocket close handshake with the specified status code and reason.
+ *
+ *
+ * Sends a close frame to the remote endpoint and ensures the end handler can fire
+ * by requesting one final frame. This allows proper cleanup and close event delivery.
+ *
+ * @param code the WebSocket close status code (e.g., 1000 for normal closure)
+ * @param reason human-readable explanation for the closure (maybe null)
+ * @return true if close was initiated, false if connection was already closed
+ */
+ @Override
+ public boolean sendClose(final int code, final String reason) {
+ if (webSocket.isClosed()) {
+ return false;
+ }
+
+ logAsyncOperationFailures(webSocket.close((short) code, reason));
+
+ // Request one frame to ensure the end handler fires for proper cleanup
+ webSocket.fetch(1);
+
+ return true;
+ }
+
+ /**
+ * Returns the number of bytes currently queued for transmission.
+ *
+ *
+ * This value represents the backpressure state of the WebSocket connection.
+ * High values indicate the remote endpoint may be slower at consuming data
+ * than this endpoint is producing it.
+ *
+ * @return the number of bytes pending transmission
+ */
+ @Override
+ public long queueSize() {
+ return pendingBytesCount.get();
+ }
+
+ /**
+ * Requests delivery of the next WebSocket frame, resuming the paused connection.
+ *
+ *
+ * This method implements the backpressure protocol by allowing the consumer
+ * to control the flow of incoming messages. Must be called after each message
+ * is processed to receive later frames.
+ *
+ *
+ * Failure to call this method will cause the WebSocket to remain paused
+ * indefinitely after the first message delivery.
+ */
+ @Override
+ public void request() {
+ webSocket.fetch(1);
+ }
+}
diff --git a/httpclient-vertx-5/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.http.HttpClient$Factory b/httpclient-vertx-5/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.http.HttpClient$Factory
new file mode 100644
index 00000000000..d9bc49c64e0
--- /dev/null
+++ b/httpclient-vertx-5/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.http.HttpClient$Factory
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Licensed 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.
+#
+
+io.fabric8.kubernetes.client.vertx.VertxHttpClientFactory
\ No newline at end of file
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/AsyncInputReaderTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/AsyncInputReaderTest.java
new file mode 100644
index 00000000000..e0352575097
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/AsyncInputReaderTest.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("AsyncInputReader")
+class AsyncInputReaderTest {
+
+ private Vertx vertx;
+
+ @BeforeEach
+ void setUp() {
+ vertx = Vertx.vertx();
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (vertx != null) {
+ vertx.close();
+ }
+ }
+
+ @Test
+ @DisplayName("Should read data chunk from InputStream")
+ void shouldReadDataChunk() throws Exception {
+ String testData = "Hello, World!";
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ AsyncInputReader reader = new AsyncInputReader(vertx, inputStream);
+
+ AtomicReference resultBuffer = new AtomicReference<>();
+ AtomicReference errorRef = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ Future future = reader.readNextChunk();
+ future.onComplete(result -> {
+ if (result.succeeded()) {
+ resultBuffer.set(result.result());
+ } else {
+ errorRef.set(result.cause());
+ }
+ latch.countDown();
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(errorRef.get()).isNull();
+ assertThat(resultBuffer.get()).isNotNull();
+ assertThat(resultBuffer.get().toString()).hasToString(testData);
+ }
+
+ @Test
+ @DisplayName("Should return null on EOF")
+ void shouldReturnNullOnEOF() throws Exception {
+ InputStream inputStream = new ByteArrayInputStream(new byte[0]);
+ AsyncInputReader reader = new AsyncInputReader(vertx, inputStream);
+
+ AtomicReference resultBuffer = new AtomicReference<>();
+ AtomicReference errorRef = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ Future future = reader.readNextChunk();
+ future.onComplete(result -> {
+ if (result.succeeded()) {
+ resultBuffer.set(result.result());
+ } else {
+ errorRef.set(result.cause());
+ }
+ latch.countDown();
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(errorRef.get()).isNull();
+ assertThat(resultBuffer.get()).isNull();
+ }
+
+ @Test
+ @DisplayName("Should handle IOException from InputStream")
+ void shouldHandleIOException() throws Exception {
+ InputStream inputStream = new InputStream() {
+ @Override
+ public int read() throws IOException {
+ throw new IOException("Test IO exception");
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ throw new IOException("Test IO exception");
+ }
+ };
+ AsyncInputReader reader = new AsyncInputReader(vertx, inputStream);
+
+ AtomicReference resultBuffer = new AtomicReference<>();
+ AtomicReference errorRef = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ Future future = reader.readNextChunk();
+ future.onComplete(result -> {
+ if (result.succeeded()) {
+ resultBuffer.set(result.result());
+ } else {
+ errorRef.set(result.cause());
+ }
+ latch.countDown();
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(resultBuffer.get()).isNull();
+ assertThat(errorRef.get()).isNotNull();
+ assertThat(errorRef.get()).isInstanceOf(IOException.class);
+ assertThat(errorRef.get().getMessage()).isEqualTo("Test IO exception");
+ }
+
+ @Test
+ @DisplayName("Should handle large data streams in chunks")
+ void shouldHandleLargeDataInChunks() throws Exception {
+ // Create 5KB of test data (larger than CHUNK_SIZE of 2048)
+ byte[] largeData = new byte[5 * 1024];
+ for (int i = 0; i < largeData.length; i++) {
+ largeData[i] = (byte) (i % 256);
+ }
+ InputStream inputStream = new ByteArrayInputStream(largeData);
+ AsyncInputReader reader = new AsyncInputReader(vertx, inputStream);
+
+ AtomicReference resultBuffer = new AtomicReference<>();
+ AtomicReference errorRef = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ Future future = reader.readNextChunk();
+ future.onComplete(result -> {
+ if (result.succeeded()) {
+ resultBuffer.set(result.result());
+ } else {
+ errorRef.set(result.cause());
+ }
+ latch.countDown();
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(errorRef.get()).isNull();
+ assertThat(resultBuffer.get()).isNotNull();
+ // Should read-only up to CHUNK_SIZE (2048 bytes)
+ assertThat(resultBuffer.get().length()).isEqualTo(2048);
+ }
+
+ @Test
+ @DisplayName("Should read multiple chunks sequentially")
+ void shouldReadMultipleChunksSequentially() throws Exception {
+ String testData = "A".repeat(4096); // 4KB of data (2 chunks)
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ AsyncInputReader reader = new AsyncInputReader(vertx, inputStream);
+
+ // Read first chunk
+ AtomicReference firstBuffer = new AtomicReference<>();
+ CountDownLatch firstLatch = new CountDownLatch(1);
+
+ reader.readNextChunk().onComplete(result -> {
+ if (result.succeeded()) {
+ firstBuffer.set(result.result());
+ }
+ firstLatch.countDown();
+ });
+
+ assertThat(firstLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(firstBuffer.get()).isNotNull();
+ assertThat(firstBuffer.get().length()).isEqualTo(2048);
+
+ // Read second chunk
+ AtomicReference secondBuffer = new AtomicReference<>();
+ CountDownLatch secondLatch = new CountDownLatch(1);
+
+ reader.readNextChunk().onComplete(result -> {
+ if (result.succeeded()) {
+ secondBuffer.set(result.result());
+ }
+ secondLatch.countDown();
+ });
+
+ assertThat(secondLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(secondBuffer.get()).isNotNull();
+ assertThat(secondBuffer.get().length()).isEqualTo(2048);
+
+ // Read third chunk (should be EOF)
+ AtomicReference thirdBuffer = new AtomicReference<>();
+ CountDownLatch thirdLatch = new CountDownLatch(1);
+
+ reader.readNextChunk().onComplete(result -> {
+ if (result.succeeded()) {
+ thirdBuffer.set(result.result());
+ }
+ thirdLatch.countDown();
+ });
+
+ assertThat(thirdLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(thirdBuffer.get()).isNull(); // EOF
+ }
+
+ @Test
+ @DisplayName("Should reuse read buffer across multiple reads")
+ void shouldReuseReadBuffer() throws Exception {
+ String testData1 = "First chunk data";
+ String testData2 = "Second chunk data";
+
+ // First read
+ InputStream inputStream1 = new ByteArrayInputStream(testData1.getBytes());
+ AsyncInputReader reader = new AsyncInputReader(vertx, inputStream1);
+
+ AtomicReference firstResult = new AtomicReference<>();
+ CountDownLatch firstLatch = new CountDownLatch(1);
+
+ reader.readNextChunk().onComplete(result -> {
+ if (result.succeeded()) {
+ firstResult.set(result.result());
+ }
+ firstLatch.countDown();
+ });
+
+ assertThat(firstLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(firstResult.get()).isNotNull();
+
+ // Create new reader with different stream but same instance should reuse buffer
+ InputStream inputStream2 = new ByteArrayInputStream(testData2.getBytes());
+ AsyncInputReader reader2 = new AsyncInputReader(vertx, inputStream2);
+
+ AtomicReference secondResult = new AtomicReference<>();
+ CountDownLatch secondLatch = new CountDownLatch(1);
+
+ reader2.readNextChunk().onComplete(result -> {
+ if (result.succeeded()) {
+ secondResult.set(result.result());
+ }
+ secondLatch.countDown();
+ });
+
+ assertThat(secondLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(secondResult.get()).isNotNull();
+ assertThat(secondResult.get().toString()).hasToString(testData2);
+ }
+
+ @Test
+ @DisplayName("Should handle partial reads correctly")
+ void shouldHandlePartialReads() throws Exception {
+ // Custom InputStream that returns only 1 byte at a time
+ AsyncInputReader reader = getAsyncInputReader();
+
+ AtomicReference resultBuffer = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ reader.readNextChunk().onComplete(result -> {
+ if (result.succeeded()) {
+ resultBuffer.set(result.result());
+ }
+ latch.countDown();
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(resultBuffer.get()).isNotNull();
+ assertThat(resultBuffer.get().length()).isEqualTo(1);
+ assertThat(resultBuffer.get().toString()).hasToString("H");
+ }
+
+ private AsyncInputReader getAsyncInputReader() {
+ InputStream inputStream = new InputStream() {
+ private final byte[] data = "Hello".getBytes();
+ private int position = 0;
+
+ @Override
+ public int read() {
+ return (position < data.length ? data[position++] : -1) & 0xFF;
+ }
+
+ @Override
+ public int read(byte[] b) {
+ if (position >= data.length) {
+ return -1;
+ }
+ // Only return 1 byte at a time
+ b[0] = data[position++];
+ return 1;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) {
+ return read(b); // Delegate to the single-byte read method
+ }
+ };
+
+ return new AsyncInputReader(vertx, inputStream);
+ }
+
+ @Test
+ @DisplayName("Should handle concurrent reads safely")
+ void shouldHandleConcurrentReadsSafely() throws Exception {
+ String testData = "Concurrent test data";
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ AsyncInputReader reader = new AsyncInputReader(vertx, inputStream);
+
+ CountDownLatch completionLatch = new CountDownLatch(2);
+ AtomicReference result1 = new AtomicReference<>();
+ AtomicReference result2 = new AtomicReference<>();
+
+ // Start two concurrent reads
+ reader.readNextChunk().onComplete(ar -> {
+ if (ar.succeeded()) {
+ result1.set(ar.result());
+ }
+ completionLatch.countDown();
+ });
+
+ reader.readNextChunk().onComplete(ar -> {
+ if (ar.succeeded()) {
+ result2.set(ar.result());
+ }
+ completionLatch.countDown();
+ });
+
+ assertThat(completionLatch.await(5, TimeUnit.SECONDS)).isTrue();
+
+ // One of the reads should succeed with data, the other should get EOF or different data
+ // This tests that the buffer is handled safely under concurrent access
+ boolean oneSucceeded = (result1.get() != null && result1.get().length() > 0) ||
+ (result2.get() != null && result2.get().length() > 0);
+ assertThat(oneSucceeded).isTrue();
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/HttpBodyStreamTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/HttpBodyStreamTest.java
new file mode 100644
index 00000000000..9c36ee50fdb
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/HttpBodyStreamTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.HttpClient;
+import io.fabric8.kubernetes.client.http.HttpRequest;
+import io.fabric8.kubernetes.client.http.HttpResponse;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.http.StreamResetException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+class HttpBodyStreamTest {
+
+ private Vertx vertx;
+ private HttpServer server;
+ private volatile Handler requestHandler;
+ private HttpClient.Factory clientFactory = new VertxHttpClientFactory();
+
+ @BeforeEach
+ public void before() throws Exception {
+ vertx = Vertx.vertx();
+ server = vertx.createHttpServer().requestHandler(req -> {
+ Handler handler = requestHandler;
+ if (handler != null) {
+ handler.handle(req);
+ } else {
+ req.response().setStatusCode(404).end();
+ }
+ });
+ server.listen(8080).toCompletionStage().toCompletableFuture().get(20, TimeUnit.SECONDS);
+ }
+
+ @AfterEach
+ public void after() throws Exception {
+ vertx.close().toCompletionStage().toCompletableFuture().get(20, TimeUnit.SECONDS);
+ }
+
+ @Test
+ void testStreamFlowControl() throws Exception {
+
+ AtomicInteger bytesSent = new AtomicInteger();
+ AtomicInteger data = new AtomicInteger('A');
+ AtomicInteger bytesReceived = new AtomicInteger();
+
+ requestHandler = req -> {
+ req.pause();
+ AtomicInteger last = new AtomicInteger();
+ vertx.setPeriodic(100, id -> {
+ int val = bytesSent.get();
+ if (data.get() != -1 && val - last.get() == 0) {
+ req.resume();
+ data.set(-1);
+ }
+ last.set(val);
+ });
+ req.handler(chunk -> {
+ bytesReceived.addAndGet(chunk.length());
+ });
+ req.endHandler(v -> {
+ req.response().end();
+ });
+ };
+ HttpClient.Builder builder = clientFactory.newBuilder();
+ HttpClient client = builder.build();
+
+ HttpRequest request = client.newHttpRequestBuilder().uri("http://localhost:8080").post("text/plain", new InputStream() {
+ @Override
+ public int read() throws IOException {
+ int ret = data.get();
+ if (ret != -1) {
+ bytesSent.incrementAndGet();
+ }
+ return ret;
+ }
+ }, -1L).build();
+ HttpResponse resp = client.sendAsync(request, String.class).get(10, TimeUnit.SECONDS);
+ assertEquals(bytesSent.get(), bytesReceived.get());
+ }
+
+ @Test
+ void testStreamError() throws Exception {
+
+ requestHandler = req -> {
+ };
+ HttpClient.Builder builder = clientFactory.newBuilder();
+ HttpClient client = builder.build();
+
+ HttpRequest request = client.newHttpRequestBuilder().uri("http://localhost:8080").post("text/plain", new InputStream() {
+ int bytesSent = 0;
+
+ @Override
+ public int read() throws IOException {
+ if (bytesSent++ < 10_000) {
+ return 'A';
+ } else {
+ throw new IOException("Cannot read");
+ }
+ }
+ }, -1L).build();
+ try {
+ client.sendAsync(request, String.class).get(10, TimeUnit.SECONDS);
+ fail();
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ assertEquals(StreamResetException.class, cause.getClass());
+ }
+ }
+
+ @Test
+ void testStackOverflow() throws Exception {
+
+ requestHandler = req -> {
+ AtomicInteger size = new AtomicInteger();
+ req.handler(buff -> size.addAndGet(buff.length()));
+ req.endHandler(v -> {
+ req.response().end("" + size);
+ });
+ };
+ HttpClient.Builder builder = clientFactory.newBuilder();
+ HttpClient client = builder.build();
+
+ int contentLength = 128_000;
+ InputStream is = new InputStream() {
+ int remaining = contentLength;
+
+ @Override
+ public int read() {
+ if (remaining == 0) {
+ return -1;
+ }
+ return 'A';
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) {
+ if (remaining > 0) {
+ int size = Math.min(len, remaining);
+ remaining -= size;
+ for (int i = 0; i < size; i++) {
+ b[off++] = 'A';
+ }
+ return size;
+ } else {
+ return -1;
+ }
+ }
+ };
+
+ HttpRequest request = client.newHttpRequestBuilder().uri("http://localhost:8080").post("text/plain", is, -1).build();
+ HttpResponse resp = client.sendAsync(request, String.class).get(10, TimeUnit.SECONDS);
+ int val = Integer.parseInt(resp.body());
+ assertEquals(contentLength, val);
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/InputStreamReadStreamTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/InputStreamReadStreamTest.java
new file mode 100644
index 00000000000..c320071c128
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/InputStreamReadStreamTest.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.streams.ReadStream;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("InputStreamReadStream")
+class InputStreamReadStreamTest {
+
+ private Vertx vertx;
+ private VertxHttpRequest vertxHttpRequest;
+
+ @Mock
+ private HttpClientRequest httpRequest;
+
+ @Captor
+ private ArgumentCaptor resetCodeCaptor;
+
+ @Captor
+ private ArgumentCaptor resetCauseCaptor;
+
+ private AutoCloseable mocks;
+
+ @BeforeEach
+ void setUp() {
+ mocks = MockitoAnnotations.openMocks(this);
+ vertx = Vertx.vertx();
+ vertxHttpRequest = new VertxHttpRequest(vertx, null, null);
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ if (mocks != null) {
+ mocks.close();
+ }
+ if (vertx != null) {
+ vertx.close();
+ }
+ }
+
+ @Test
+ @DisplayName("Should read data chunks from InputStream and emit them as buffers")
+ void shouldReadDataChunksAndEmitBuffers() throws Exception {
+ String testData = "Hello, World! This is test data for the InputStreamReadStream.";
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ List receivedBuffers = new ArrayList<>();
+ AtomicBoolean endCalled = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ readStream
+ .handler(receivedBuffers::add)
+ .endHandler(v -> {
+ endCalled.set(true);
+ latch.countDown();
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(endCalled.get()).isTrue();
+ assertThat(receivedBuffers).isNotEmpty();
+
+ // Verify that all data was read correctly
+ StringBuilder result = new StringBuilder();
+ for (Buffer buffer : receivedBuffers) {
+ result.append(buffer.toString());
+ }
+ assertThat(result.toString()).hasToString(testData);
+ }
+
+ @Test
+ @DisplayName("Should handle empty InputStream")
+ void shouldHandleEmptyInputStream() throws Exception {
+ InputStream inputStream = new ByteArrayInputStream(new byte[0]);
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ List receivedBuffers = new ArrayList<>();
+ AtomicBoolean endCalled = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ readStream
+ .handler(receivedBuffers::add)
+ .endHandler(v -> {
+ endCalled.set(true);
+ latch.countDown();
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(endCalled.get()).isTrue();
+ assertThat(receivedBuffers).isEmpty();
+ }
+
+ @Test
+ @DisplayName("Should handle IOException from InputStream")
+ void shouldHandleIOException() throws Exception {
+ InputStream inputStream = new InputStream() {
+ @Override
+ public int read() throws IOException {
+ throw new IOException("Test IO exception");
+ }
+ };
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ AtomicReference exceptionRef = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ readStream
+ .exceptionHandler(ex -> {
+ exceptionRef.set(ex);
+ latch.countDown();
+ })
+ .handler(buffer -> {
+ // Should not be called
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(exceptionRef.get()).isNotNull();
+
+ await().atMost(1, TimeUnit.SECONDS)
+ .untilAsserted(() -> verify(httpRequest, times(1)).reset(anyLong(), any(Throwable.class)));
+ }
+
+ @Test
+ @DisplayName("Should support pause and resume operations")
+ void shouldSupportPauseAndResume() throws Exception {
+ String testData = "Test data for pause/resume functionality";
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ List receivedBuffers = new ArrayList<>();
+ AtomicBoolean endCalled = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ readStream.handler(receivedBuffers::add);
+ readStream.endHandler(v -> {
+ endCalled.set(true);
+ latch.countDown();
+ });
+
+ // Pause immediately (this should still work)
+ readStream.pause();
+
+ // Resume to continue reading
+ readStream.resume();
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(endCalled.get()).isTrue();
+ assertThat(receivedBuffers).isNotEmpty();
+ }
+
+ @Test
+ @DisplayName("Should support fetch operation")
+ void shouldSupportFetch() throws Exception {
+ String testData = "Test data for fetch functionality";
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ List receivedBuffers = new ArrayList<>();
+ AtomicBoolean endCalled = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ readStream
+ .handler(receivedBuffers::add)
+ .endHandler(v -> {
+ endCalled.set(true);
+ latch.countDown();
+ });
+
+ readStream.fetch(10); // Fetch some amount
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(endCalled.get()).isTrue();
+ assertThat(receivedBuffers).isNotEmpty();
+ }
+
+ @Test
+ @DisplayName("Should handle large data streams")
+ void shouldHandleLargeDataStreams() throws Exception {
+ // Create 10KB of test data
+ byte[] largeData = new byte[10 * 1024];
+ for (int i = 0; i < largeData.length; i++) {
+ largeData[i] = (byte) (i % 256);
+ }
+ InputStream inputStream = new ByteArrayInputStream(largeData);
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ List receivedBuffers = new ArrayList<>();
+ AtomicBoolean endCalled = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ readStream
+ .handler(receivedBuffers::add)
+ .endHandler(v -> {
+ endCalled.set(true);
+ latch.countDown();
+ });
+
+ assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
+ assertThat(endCalled.get()).isTrue();
+ assertThat(receivedBuffers).isNotEmpty();
+
+ // Verify all data was read
+ int totalBytes = receivedBuffers.stream().mapToInt(Buffer::length).sum();
+ assertThat(totalBytes).isEqualTo(largeData.length);
+ }
+
+ @Test
+ @DisplayName("Should allow setting null handler to stop reading")
+ void shouldAllowNullHandler() {
+ String testData = "Test data";
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ // Set a handler first, then set to null
+ readStream.handler(buffer -> {
+ // Initial handler
+ });
+
+ ReadStream result = readStream.handler(null);
+
+ assertThat(result).isSameAs(readStream);
+ // No exception should be thrown
+ }
+
+ @Test
+ @DisplayName("Should call exception handler when provided")
+ void shouldCallExceptionHandlerWhenProvided() throws Exception {
+ InputStream inputStream = new InputStream() {
+ @Override
+ public int read() throws IOException {
+ throw new IOException("Simulated IO error");
+ }
+ };
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ AtomicReference caughtException = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ readStream
+ .exceptionHandler(ex -> {
+ caughtException.set(ex);
+ latch.countDown();
+ })
+ .handler(buffer -> {
+ // Should not be called due to exception
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(caughtException.get()).isNotNull();
+ assertThat(caughtException.get()).isInstanceOf(IOException.class);
+
+ await().atMost(1, TimeUnit.SECONDS)
+ .untilAsserted(() -> verify(httpRequest).reset(resetCodeCaptor.capture(), resetCauseCaptor.capture()));
+ assertThat(resetCodeCaptor.getValue()).isZero();
+ assertThat(resetCauseCaptor.getValue()).isSameAs(caughtException.get());
+ }
+
+ @Test
+ @DisplayName("Should handle backpressure correctly")
+ void shouldHandleBackpressure() throws Exception {
+ // Slow consuming input stream
+ String testData = "A".repeat(5000); // 5KB of data
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ List receivedBuffers = new ArrayList<>();
+ AtomicInteger handlerCallCount = new AtomicInteger(0);
+ AtomicBoolean endCalled = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ //Handler with artificial delay to simulate slow consumption
+ readStream
+ .handler(buffer -> {
+ handlerCallCount.incrementAndGet();
+ receivedBuffers.add(buffer);
+ // Small delay to simulate processing
+ await().pollDelay(Duration.ofMillis(1)).until(() -> true);
+ })
+ .endHandler(v -> {
+ endCalled.set(true);
+ latch.countDown();
+ });
+
+ assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
+ assertThat(endCalled.get()).isTrue();
+ assertThat(receivedBuffers).isNotEmpty();
+ assertThat(handlerCallCount.get()).isGreaterThan(0);
+ }
+
+ @Test
+ @DisplayName("Should handle pause and resume with proper flow control")
+ void shouldHandlePauseResumeFlowControl() throws Exception {
+ String testData = "Test data for pause functionality";
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ InputStreamReadStream readStream = new InputStreamReadStream(vertxHttpRequest, inputStream, httpRequest);
+
+ List receivedBuffers = new ArrayList<>();
+ AtomicBoolean endCalled = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ // Set handler and end handler first
+ readStream.handler(receivedBuffers::add);
+ readStream.endHandler(v -> {
+ endCalled.set(true);
+ latch.countDown();
+ });
+
+ // Pause and resume to test flow control
+ readStream.pause();
+ readStream.resume();
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(endCalled.get()).isTrue();
+ assertThat(receivedBuffers).isNotEmpty();
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/SslTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/SslTest.java
new file mode 100644
index 00000000000..2649b5cd0ba
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/SslTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.HttpClient;
+import io.fabric8.kubernetes.client.http.HttpRequest;
+import io.fabric8.kubernetes.client.http.HttpResponse;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.net.SelfSignedCertificate;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SslTest {
+
+ private Vertx vertx;
+ private HttpServer server;
+ private volatile Handler requestHandler;
+ private HttpClient.Factory clientFactory = new VertxHttpClientFactory();
+ private TrustManager[] trustManagers;
+
+ @BeforeEach
+ public void before() throws Exception {
+ SelfSignedCertificate cert = SelfSignedCertificate.create();
+ vertx = Vertx.vertx();
+ server = vertx.createHttpServer(new HttpServerOptions()
+ .setSsl(true)
+ .setKeyCertOptions(cert.keyCertOptions()))
+ .requestHandler(req -> {
+ Handler handler = requestHandler;
+ if (handler != null) {
+ handler.handle(req);
+ } else {
+ req.response().setStatusCode(404).end();
+ }
+ });
+ server.listen(8443).toCompletionStage().toCompletableFuture().get(20, TimeUnit.SECONDS);
+ TrustManagerFactory tmf = cert.trustOptions().getTrustManagerFactory(vertx);
+ trustManagers = tmf.getTrustManagers();
+ }
+
+ @AfterEach
+ public void after() throws Exception {
+ vertx.close().toCompletionStage().toCompletableFuture().get(20, TimeUnit.SECONDS);
+ }
+
+ @Test
+ void testGet() throws Exception {
+ requestHandler = req -> {
+ req.response().end("OK");
+ };
+ HttpClient.Builder builder = clientFactory.newBuilder().sslContext(null, trustManagers);
+ HttpClient client = builder.build();
+ HttpRequest request = client.newHttpRequestBuilder().uri("https://localhost:8443").build();
+ HttpResponse resp = client.sendAsync(request, String.class).get(10, TimeUnit.SECONDS);
+ assertEquals(200, resp.code());
+ assertEquals("OK", resp.bodyString());
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/StackBasedRecursionGuardTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/StackBasedRecursionGuardTest.java
new file mode 100644
index 00000000000..dc704938a3c
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/StackBasedRecursionGuardTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("StackBasedRecursionGuard")
+class StackBasedRecursionGuardTest {
+
+ @Test
+ @DisplayName("allows recursion below the limit")
+ void belowLimit() {
+ StackBasedRecursionGuard guard = new StackBasedRecursionGuard();
+ try {
+ assertThat(guard.enter()).isTrue();
+ assertThat(guard.getCurrentDepth()).isEqualTo(1);
+ } finally {
+ guard.exit();
+ }
+ assertThat(guard.getCurrentDepth()).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("returns false when depth exceeds limit")
+ void exceedsLimit() {
+ StackBasedRecursionGuard guard = new StackBasedRecursionGuard();
+ AtomicBoolean hitFalse = new AtomicBoolean(false);
+
+ // Manually create a scenario where we exceed the limit
+ for (int i = 0; i < 8; i++) {
+ assertThat(guard.enter()).isTrue();
+ }
+
+ // The 9th enter should return false
+ assertThat(guard.enter()).isFalse();
+ hitFalse.set(true);
+
+ // Clean up all levels (including the failed one)
+ for (int i = 0; i < 9; i++) {
+ guard.exit();
+ }
+
+ assertThat(hitFalse.get()).isTrue();
+ assertThat(guard.getCurrentDepth()).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("tracks depth correctly")
+ void tracksDepth() {
+ StackBasedRecursionGuard guard = new StackBasedRecursionGuard();
+
+ assertThat(guard.getCurrentDepth()).isZero();
+
+ assertThat(guard.enter()).isTrue(); // depth = 1
+ assertThat(guard.getCurrentDepth()).isEqualTo(1);
+
+ assertThat(guard.enter()).isTrue(); // depth = 2
+ assertThat(guard.getCurrentDepth()).isEqualTo(2);
+
+ guard.exit(); // depth = 1
+ assertThat(guard.getCurrentDepth()).isEqualTo(1);
+
+ guard.exit(); // depth = 0
+ assertThat(guard.getCurrentDepth()).isZero();
+ }
+
+ @Test
+ @DisplayName("handles nested enter/exit correctly")
+ void nestedEnterExit() {
+ StackBasedRecursionGuard guard = new StackBasedRecursionGuard();
+
+ // First level
+ assertThat(guard.enter()).isTrue();
+ try {
+ assertThat(guard.getCurrentDepth()).isEqualTo(1);
+
+ // Second level
+ assertThat(guard.enter()).isTrue();
+ try {
+ assertThat(guard.getCurrentDepth()).isEqualTo(2);
+
+ // Third level
+ assertThat(guard.enter()).isTrue();
+ try {
+ assertThat(guard.getCurrentDepth()).isEqualTo(3);
+ } finally {
+ guard.exit();
+ }
+ assertThat(guard.getCurrentDepth()).isEqualTo(2);
+ } finally {
+ guard.exit();
+ }
+ assertThat(guard.getCurrentDepth()).isEqualTo(1);
+ } finally {
+ guard.exit();
+ }
+ assertThat(guard.getCurrentDepth()).isZero();
+ }
+
+ @Test
+ @DisplayName("resets depth after failed enter")
+ void resetsAfterFailedEnter() {
+ StackBasedRecursionGuard guard = new StackBasedRecursionGuard();
+
+ // Fill up to the limit
+ for (int i = 0; i < 8; i++) {
+ assertThat(guard.enter()).isTrue();
+ }
+ assertThat(guard.getCurrentDepth()).isEqualTo(8);
+
+ // Next enter should fail but still increment depth internally
+ assertThat(guard.enter()).isFalse();
+ assertThat(guard.getCurrentDepth()).isEqualTo(9);
+
+ // Exit all levels
+ for (int i = 0; i < 9; i++) {
+ guard.exit();
+ }
+ assertThat(guard.getCurrentDepth()).isZero();
+
+ // Should work again
+ assertThat(guard.enter()).isTrue();
+ guard.exit();
+ assertThat(guard.getCurrentDepth()).isZero();
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/StreamFlowControllerTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/StreamFlowControllerTest.java
new file mode 100644
index 00000000000..77419d7d704
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/StreamFlowControllerTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.vertx.core.Context;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("StreamFlowController")
+class StreamFlowControllerTest {
+
+ private Vertx vertx;
+ private Context context;
+ private StreamFlowController controller;
+
+ @BeforeEach
+ void setUp() {
+ vertx = Vertx.vertx();
+ context = vertx.getOrCreateContext();
+ controller = new StreamFlowController();
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (vertx != null) {
+ vertx.close();
+ }
+ }
+
+ @Test
+ @DisplayName("Should not be initialized before initialize() is called")
+ void shouldNotBeInitializedBeforeInitialize() {
+ assertThat(controller.isInitialized()).isFalse();
+ }
+
+ @Test
+ @DisplayName("Should be initialized after initialize() is called")
+ void shouldBeInitializedAfterInitialize() {
+ controller.initialize(context, () -> {
+ });
+
+ assertThat(controller.isInitialized()).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should handle data chunks and end handler in Vert.x context")
+ void shouldHandleDataChunksAndEndHandlerInVertxContext() throws Exception {
+ List receivedBuffers = new ArrayList<>();
+ AtomicBoolean endHandlerCalled = new AtomicBoolean(false);
+ CountDownLatch dataLatch = new CountDownLatch(2);
+ CountDownLatch endLatch = new CountDownLatch(1);
+
+ vertx.runOnContext(v -> {
+ controller.initialize(context, () -> {
+ });
+ controller.configureDataHandler(buffer -> {
+ receivedBuffers.add(buffer);
+ dataLatch.countDown();
+ });
+ controller.setEndHandler(h -> {
+ endHandlerCalled.set(true);
+ endLatch.countDown();
+ });
+
+ // Write data chunks
+ controller.writeChunk(Buffer.buffer("Hello"));
+ controller.writeChunk(Buffer.buffer("World"));
+
+ // Write end sentinel
+ controller.writeEndSentinel();
+ });
+
+ assertThat(dataLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(endLatch.await(5, TimeUnit.SECONDS)).isTrue();
+
+ assertThat(receivedBuffers).hasSize(2);
+ assertThat(receivedBuffers.get(0).toString()).hasToString("Hello");
+ assertThat(receivedBuffers.get(1).toString()).hasToString("World");
+ assertThat(endHandlerCalled.get()).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should handle null handlers gracefully")
+ void shouldHandleNullHandlersGracefully() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ vertx.runOnContext(v -> {
+ controller.initialize(context, () -> {
+ });
+ controller.configureDataHandler(null);
+ controller.setEndHandler(null);
+
+ // Should not throw exceptions
+ controller.writeChunk(Buffer.buffer("test"));
+ controller.writeEndSentinel();
+
+ latch.countDown();
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should handle multiple end sentinels")
+ void shouldHandleMultipleEndSentinels() throws Exception {
+ AtomicBoolean endHandlerCalled = new AtomicBoolean(false);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ vertx.runOnContext(v -> {
+ controller.initialize(context, () -> {
+ });
+ controller.configureDataHandler(buffer -> {
+ });
+ controller.setEndHandler(h -> {
+ endHandlerCalled.set(true);
+ latch.countDown();
+ });
+
+ // Write multiple end sentinels - should call handler for first one
+ controller.writeEndSentinel();
+ controller.writeEndSentinel();
+ });
+
+ assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(endHandlerCalled.get()).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should handle complex flow control operations")
+ void shouldHandleComplexFlowControlOperations() throws Exception {
+ List receivedBuffers = new ArrayList<>();
+ AtomicBoolean endHandlerCalled = new AtomicBoolean(false);
+ CountDownLatch dataLatch = new CountDownLatch(3);
+ CountDownLatch endLatch = new CountDownLatch(1);
+
+ vertx.runOnContext(v -> {
+ controller.initialize(context, () -> {
+ });
+ controller.configureDataHandler(buffer -> {
+ receivedBuffers.add(buffer);
+ dataLatch.countDown();
+ });
+ controller.setEndHandler(h -> {
+ endHandlerCalled.set(true);
+ endLatch.countDown();
+ });
+
+ // Complex sequence of operations
+ controller.writeChunk(Buffer.buffer("first"));
+ controller.pause();
+ controller.writeChunk(Buffer.buffer("second"));
+ controller.resume();
+ controller.fetch(1);
+ controller.writeChunk(Buffer.buffer("third"));
+ controller.writeEndSentinel();
+ });
+
+ assertThat(dataLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(endLatch.await(5, TimeUnit.SECONDS)).isTrue();
+
+ assertThat(receivedBuffers).hasSize(3);
+ assertThat(receivedBuffers.get(0).toString()).hasToString("first");
+ assertThat(receivedBuffers.get(1).toString()).hasToString("second");
+ assertThat(receivedBuffers.get(2).toString()).hasToString("third");
+ assertThat(endHandlerCalled.get()).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should properly separate end sentinel from data")
+ void shouldProperlySeparateEndSentinelFromData() throws Exception {
+ List receivedBuffers = new ArrayList<>();
+ AtomicBoolean endHandlerCalled = new AtomicBoolean(false);
+ CountDownLatch dataLatch = new CountDownLatch(2);
+ CountDownLatch endLatch = new CountDownLatch(1);
+
+ vertx.runOnContext(v -> {
+ controller.initialize(context, () -> {
+ });
+ controller.configureDataHandler(buffer -> {
+ // Should only receive actual data, not the end sentinel
+ receivedBuffers.add(buffer);
+ dataLatch.countDown();
+ });
+ controller.setEndHandler(h -> {
+ endHandlerCalled.set(true);
+ endLatch.countDown();
+ });
+
+ // Write data and then end sentinel
+ controller.writeChunk(Buffer.buffer("data1"));
+ controller.writeChunk(Buffer.buffer("data2"));
+ controller.writeEndSentinel();
+ });
+
+ assertThat(dataLatch.await(5, TimeUnit.SECONDS)).isTrue();
+ assertThat(endLatch.await(5, TimeUnit.SECONDS)).isTrue();
+
+ // Should only have received data chunks, not the end sentinel
+ assertThat(receivedBuffers).hasSize(2);
+ assertThat(receivedBuffers.get(0).toString()).hasToString("data1");
+ assertThat(receivedBuffers.get(1).toString()).hasToString("data2");
+ assertThat(endHandlerCalled.get()).isTrue();
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxAsyncBodyTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxAsyncBodyTest.java
new file mode 100644
index 00000000000..5b896446be6
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxAsyncBodyTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AsyncBody;
+import io.vertx.core.Handler;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@DisplayName("VertxAsyncBody")
+class VertxAsyncBodyTest {
+
+ @Mock
+ private HttpClientResponse response;
+
+ @Mock
+ private HttpClientRequest request;
+
+ @Mock
+ private AsyncBody.Consumer> consumer;
+
+ @Captor
+ private ArgumentCaptor> dataHandlerCaptor;
+
+ @Captor
+ private ArgumentCaptor> endHandlerCaptor;
+
+ @Captor
+ private ArgumentCaptor> exceptionHandlerCaptor;
+
+ @Captor
+ private ArgumentCaptor> listBufferCaptor;
+
+ private VertxAsyncBody asyncBody;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+
+ when(response.handler(dataHandlerCaptor.capture())).thenReturn(response);
+ when(response.endHandler(endHandlerCaptor.capture())).thenReturn(response);
+ when(response.exceptionHandler(exceptionHandlerCaptor.capture())).thenReturn(response);
+ when(response.request()).thenReturn(request);
+ }
+
+ @Test
+ @DisplayName("Should setup data, end, and exception handlers during construction")
+ void constructor_shouldSetupHandlersCorrectly() {
+ asyncBody = new VertxAsyncBody(response, consumer);
+
+ verify(response).handler(Mockito.any());
+ verify(response).endHandler(Mockito.any());
+ verify(response).exceptionHandler(Mockito.any());
+
+ assertThat(dataHandlerCaptor.getValue()).isNotNull();
+ assertThat(endHandlerCaptor.getValue()).isNotNull();
+ assertThat(exceptionHandlerCaptor.getValue()).isNotNull();
+ }
+
+ @Test
+ @DisplayName("Should call fetch(1) on response when consume is called")
+ void consume_shouldCallFetchOnResponse() {
+ asyncBody = new VertxAsyncBody(response, consumer);
+
+ asyncBody.consume();
+
+ verify(response).fetch(1);
+ }
+
+ @Test
+ @DisplayName("Should convert Vert.x buffer to ByteBuffer and call consumer when data arrives")
+ void dataHandler_shouldConsumeBufferAsByteBuffer() throws Exception {
+ asyncBody = new VertxAsyncBody(response, consumer);
+ Buffer buffer = Buffer.buffer("test data");
+
+ dataHandlerCaptor.getValue().handle(buffer);
+
+ verify(consumer).consume(listBufferCaptor.capture(), Mockito.eq(asyncBody));
+
+ List capturedBuffers = listBufferCaptor.getValue();
+ assertThat(capturedBuffers).hasSize(1);
+
+ ByteBuffer capturedBuffer = capturedBuffers.get(0);
+ byte[] capturedBytes = new byte[capturedBuffer.remaining()];
+ capturedBuffer.get(capturedBytes);
+ assertThat(new String(capturedBytes)).isEqualTo("test data");
+ }
+
+ @Test
+ @DisplayName("Should reset request and complete exceptionally when consumer throws exception")
+ void dataHandler_shouldCompleteExceptionallyOnConsumerException() {
+ RuntimeException consumerException = new RuntimeException("Consumer failed");
+ asyncBody = new VertxAsyncBody(response, (data, body) -> {
+ throw consumerException;
+ });
+
+ dataHandlerCaptor.getValue().handle(Buffer.buffer("test"));
+
+ verify(request).reset();
+
+ CompletableFuture done = asyncBody.done();
+ assertThat(done).isCompletedExceptionally();
+
+ assertThatThrownBy(() -> done.get(1, TimeUnit.SECONDS))
+ .isInstanceOf(ExecutionException.class)
+ .hasCause(consumerException);
+ }
+
+ @Test
+ @DisplayName("Should complete done future when end handler is triggered")
+ void endHandler_shouldCompleteDoneFuture() throws Exception {
+ asyncBody = new VertxAsyncBody(response, consumer);
+
+ endHandlerCaptor.getValue().handle(null);
+
+ CompletableFuture done = asyncBody.done();
+ assertThat(done.get(1, TimeUnit.SECONDS)).isNull();
+ assertThat(done).isCompleted();
+ }
+
+ @Test
+ @DisplayName("Should complete exceptionally when response exception occurs")
+ void exceptionHandler_shouldCompleteExceptionallyOnResponseException() {
+ asyncBody = new VertxAsyncBody(response, consumer);
+ RuntimeException responseException = new RuntimeException("Response failed");
+
+ exceptionHandlerCaptor.getValue().handle(responseException);
+
+ CompletableFuture done = asyncBody.done();
+ assertThat(done).isCompletedExceptionally();
+
+ assertThatThrownBy(() -> done.get(1, TimeUnit.SECONDS))
+ .isInstanceOf(ExecutionException.class)
+ .hasCause(responseException);
+ }
+
+ @Test
+ @DisplayName("Should return same CompletableFuture instance on multiple done() calls")
+ void done_shouldReturnSameCompletableFuture() {
+ asyncBody = new VertxAsyncBody(response, consumer);
+
+ CompletableFuture done1 = asyncBody.done();
+ CompletableFuture done2 = asyncBody.done();
+
+ assertThat(done1).isSameAs(done2);
+ }
+
+ @Test
+ @DisplayName("Should clear handlers, reset request, and cancel future when cancelled")
+ void cancel_shouldClearHandlersAndResetRequest() {
+ asyncBody = new VertxAsyncBody(response, consumer);
+
+ asyncBody.cancel();
+
+ verify(response).handler(isNull());
+ verify(response).endHandler(isNull());
+ verify(request).reset();
+
+ CompletableFuture done = asyncBody.done();
+ assertThat(done).isCancelled();
+ }
+
+ @Test
+ @DisplayName("Should cancel done future when cancel is called")
+ void cancel_shouldCancelDoneFuture() {
+ asyncBody = new VertxAsyncBody(response, consumer);
+ CompletableFuture done = asyncBody.done();
+
+ asyncBody.cancel();
+
+ assertThat(done.isCancelled()).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should call consumer for each data chunk and complete on end")
+ void multipleDataChunks_shouldCallConsumerForEach() throws Exception {
+ asyncBody = new VertxAsyncBody(response, consumer);
+
+ dataHandlerCaptor.getValue().handle(Buffer.buffer("chunk1"));
+ dataHandlerCaptor.getValue().handle(Buffer.buffer("chunk2"));
+ endHandlerCaptor.getValue().handle(null);
+
+ // Then – the consumer must be invoked once per chunk (2 times total)
+ verify(consumer, Mockito.times(2))
+ .consume(listBufferCaptor.capture(), Mockito.eq(asyncBody));
+
+ // We captured two separate lists (one per chunk)
+ List> allBuffers = listBufferCaptor.getAllValues();
+ assertThat(allBuffers).hasSize(2);
+ assertThat(allBuffers.get(0).get(0).remaining()).isGreaterThan(0);
+ assertThat(allBuffers.get(1).get(0).remaining()).isGreaterThan(0);
+
+ CompletableFuture done = asyncBody.done();
+ assertThat(done.get(1, TimeUnit.SECONDS)).isNull();
+ }
+
+ @Test
+ @DisplayName("Should request one buffer at a time for backpressure control")
+ void backpressureHandling_shouldOnlyConsumeOneBufferAtATime() {
+ asyncBody = new VertxAsyncBody(response, consumer);
+
+ asyncBody.consume();
+ asyncBody.consume();
+ asyncBody.consume();
+
+ verify(response, times(3)).fetch(1);
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientAsyncBodyTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientAsyncBodyTest.java
new file mode 100644
index 00000000000..7b3edeb31dc
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientAsyncBodyTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractAsyncBodyTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientAsyncBodyTest extends AbstractAsyncBodyTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilderTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilderTest.java
new file mode 100644
index 00000000000..78932989c92
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientBuilderTest.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.HttpClient;
+import io.fabric8.kubernetes.client.http.TlsVersion;
+import io.vertx.core.Vertx;
+import io.vertx.core.impl.VertxImpl;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@DisplayName("VertxHttpClientBuilder")
+class VertxHttpClientBuilderTest {
+
+ @Test
+ @DisplayName("Should build client with zero timeout without issues")
+ void testZeroTimeouts() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ HttpClient.Builder builder = factory.newBuilder();
+
+ // should build and be usable without an issue
+ try (HttpClient client = builder.connectTimeout(0, TimeUnit.MILLISECONDS).build()) {
+ assertNotNull(client.newHttpRequestBuilder().uri("http://localhost").build());
+ }
+ }
+
+ @Test
+ @DisplayName("Should reuse shared Vertx instance")
+ void reusesVertxInstanceWhenSharedVertx() {
+ Vertx vertx = Vertx.vertx();
+ try (HttpClient client = new VertxHttpClientFactory(vertx).newBuilder().build()) {
+ assertThat(client)
+ .isInstanceOf(VertxHttpClient.class)
+ .extracting("vertx")
+ .isSameAs(vertx);
+ } finally {
+ vertx.close();
+ }
+ }
+
+ @Test
+ @DisplayName("Should create new Vertx instance when no shared instance provided")
+ void createsVertxInstanceWhenNoSharedVertx() {
+ try (HttpClient client = new VertxHttpClientFactory().newBuilder().build()) {
+ assertThat(client)
+ .isInstanceOf(VertxHttpClient.class)
+ .extracting("vertx")
+ .isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should not close shared Vertx instance when client is closed")
+ void doesntCloseSharedVertxInstanceWhenClientIsClosed() {
+ final Vertx vertx = Vertx.vertx();
+ final var builder = new VertxHttpClientFactory(vertx).newBuilder();
+ builder.build().close();
+ assertThat(builder.vertx)
+ .asInstanceOf(InstanceOfAssertFactories.type(VertxImpl.class))
+ .returns(false, vi -> vi.closeFuture().isClosed());
+ vertx.close();
+ }
+
+ @Test
+ @DisplayName("Should close owned Vertx instance when client is closed")
+ void closesVertxInstanceWhenClientIsClosed() {
+ final var builder = new VertxHttpClientFactory().newBuilder();
+ builder.build().close();
+ assertThat(builder.vertx)
+ .asInstanceOf(InstanceOfAssertFactories.type(VertxImpl.class))
+ .returns(true, vi -> vi.closeFuture().isClosed());
+ }
+
+ @Nested
+ @DisplayName("Proxy Configuration")
+ class ProxyConfigurationTests {
+
+ @Test
+ @DisplayName("Should configure HTTP proxy correctly")
+ void httpProxy_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.proxyAddress(new InetSocketAddress("proxy.example.com", 8080));
+ builder.proxyType(HttpClient.ProxyType.HTTP);
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should configure SOCKS4 proxy correctly")
+ void socks4Proxy_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.proxyAddress(new InetSocketAddress("socks.example.com", 1080));
+ builder.proxyType(HttpClient.ProxyType.SOCKS4);
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should configure SOCKS5 proxy correctly")
+ void socks5Proxy_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.proxyAddress(new InetSocketAddress("socks5.example.com", 1080));
+ builder.proxyType(HttpClient.ProxyType.SOCKS5);
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should handle direct connection when proxy type is DIRECT")
+ void directProxy_shouldSkipProxyConfiguration() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.proxyType(HttpClient.ProxyType.DIRECT);
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should configure proxy with basic authentication")
+ void proxyWithBasicAuth_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.proxyAddress(new InetSocketAddress("proxy.example.com", 8080));
+ builder.proxyType(HttpClient.ProxyType.HTTP);
+ builder.proxyAuthorization("Basic dXNlcjpwYXNz"); // user:pass in base64
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("SSL/TLS Configuration")
+ class SslTlsConfigurationTests {
+
+ @Test
+ @DisplayName("Should configure SSL context correctly")
+ void sslContext_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.sslContext(null, null);
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should configure TLS versions correctly")
+ void tlsVersions_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.sslContext(null, null);
+ builder.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3);
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should handle empty TLS versions array")
+ void emptyTlsVersions_shouldNotFailBuild() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.tlsVersions(); // empty array
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should handle null TLS versions")
+ void nullTlsVersions_shouldNotFailBuild() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("Timeout and HTTP Settings")
+ class TimeoutAndHttpSettingsTests {
+
+ @Test
+ @DisplayName("Should configure connect timeout correctly")
+ void connectTimeout_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.connectTimeout(5000, TimeUnit.MILLISECONDS);
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should configure follow redirects correctly")
+ void followRedirects_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.followAllRedirects();
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should configure HTTP/1.1 preference correctly")
+ void preferHttp11_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.preferHttp11();
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should handle null connect timeout")
+ void nullConnectTimeout_shouldNotFailBuild() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("WebSocket Configuration")
+ class WebSocketConfigurationTests {
+
+ @Test
+ @DisplayName("Should configure WebSocket options with SSL")
+ void webSocketWithSsl_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ builder.sslContext(null, null);
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+
+ @Test
+ @DisplayName("Should configure WebSocket options without SSL")
+ void webSocketWithoutSsl_shouldConfigureCorrectly() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("Builder Instance Management")
+ class BuilderInstanceManagementTests {
+
+ @Test
+ @DisplayName("Should create new instance with shared Vertx")
+ void newInstance_shouldShareVertx() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder originalBuilder = factory.newBuilder();
+ VertxHttpClientBuilder newBuilder = originalBuilder.newInstance(factory);
+
+ assertThat(newBuilder.vertx).isSameAs(originalBuilder.vertx);
+ }
+
+ @Test
+ @DisplayName("Should handle multiple builds from same builder")
+ void multipleBuildFromSameBuilder_shouldSucceed() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ try (HttpClient client1 = builder.build();
+ HttpClient client2 = builder.build()) {
+ assertThat(client1).isNotNull();
+ assertThat(client2).isNotNull();
+ assertThat(client1).isNotSameAs(client2);
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("Error Handling and Edge Cases")
+ class ErrorHandlingTests {
+
+ @Test
+ @DisplayName("Should handle build with all default settings")
+ void buildWithDefaults_shouldSucceed() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ assertThat(client).isInstanceOf(VertxHttpClient.class);
+ }
+ }
+
+ @Test
+ @DisplayName("Should handle complex configuration combination")
+ void complexConfiguration_shouldSucceed() {
+ VertxHttpClientFactory factory = new VertxHttpClientFactory();
+ VertxHttpClientBuilder> builder = factory.newBuilder();
+
+ InetSocketAddress proxyAddress = new InetSocketAddress("proxy.example.com", 8080);
+
+ builder
+ .connectTimeout(10000, TimeUnit.MILLISECONDS)
+ .sslContext(null, null)
+ .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
+ .proxyAddress(proxyAddress)
+ .proxyType(HttpClient.ProxyType.HTTP)
+ .followAllRedirects()
+ .preferHttp11();
+
+ try (HttpClient client = builder.build()) {
+ assertThat(client).isNotNull();
+ assertThat(client).isInstanceOf(VertxHttpClient.class);
+ }
+ }
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientGetTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientGetTest.java
new file mode 100644
index 00000000000..a38278e7cae
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientGetTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractHttpGetTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+import java.net.ConnectException;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientGetTest extends AbstractHttpGetTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+
+ @Override
+ protected Class extends Exception> getConnectionFailedExceptionType() {
+ return ConnectException.class;
+ }
+
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientInterceptorTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientInterceptorTest.java
new file mode 100644
index 00000000000..009dc1e8366
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientInterceptorTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractInterceptorTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientInterceptorTest extends AbstractInterceptorTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientNewWebSocketBuilderTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientNewWebSocketBuilderTest.java
new file mode 100644
index 00000000000..be291a4e976
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientNewWebSocketBuilderTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractHttpClientNewWebSocketBuilderTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientNewWebSocketBuilderTest extends AbstractHttpClientNewWebSocketBuilderTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientPostTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientPostTest.java
new file mode 100644
index 00000000000..c243ecc564f
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientPostTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractHttpPostTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+import java.net.ConnectException;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientPostTest extends AbstractHttpPostTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+
+ @Override
+ protected Class extends Exception> getConnectionFailedExceptionType() {
+ return ConnectException.class;
+ }
+
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyHttpsTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyHttpsTest.java
new file mode 100644
index 00000000000..c93b80b9789
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyHttpsTest.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractHttpClientProxyHttpsTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientProxyHttpsTest extends AbstractHttpClientProxyHttpsTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyTest.java
new file mode 100644
index 00000000000..463451e943b
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientProxyTest.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractHttpClientProxyTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientProxyTest extends AbstractHttpClientProxyTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientPutTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientPutTest.java
new file mode 100644
index 00000000000..cc5141efe14
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientPutTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractHttpPutTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+import java.net.ConnectException;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientPutTest extends AbstractHttpPutTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+
+ @Override
+ protected Class extends Exception> getConnectionFailedExceptionType() {
+ return ConnectException.class;
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientSimultaneousConnectionsTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientSimultaneousConnectionsTest.java
new file mode 100644
index 00000000000..34b955f66a8
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientSimultaneousConnectionsTest.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractSimultaneousConnectionsTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientSimultaneousConnectionsTest extends AbstractSimultaneousConnectionsTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientWebSocketSendTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientWebSocketSendTest.java
new file mode 100644
index 00000000000..65438332ca3
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpClientWebSocketSendTest.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractWebSocketSendTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpClientWebSocketSendTest extends AbstractWebSocketSendTest {
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpConfiguredClientTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpConfiguredClientTest.java
new file mode 100644
index 00000000000..123ce41b380
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpConfiguredClientTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractConfiguredClientTest;
+import io.fabric8.kubernetes.client.http.HttpClient.Factory;
+
+class VertxHttpConfiguredClientTest extends AbstractConfiguredClientTest {
+
+ @Override
+ protected Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpLoggingInterceptorTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpLoggingInterceptorTest.java
new file mode 100644
index 00000000000..e37ef08f5e7
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpLoggingInterceptorTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AbstractHttpLoggingInterceptorTest;
+import io.fabric8.kubernetes.client.http.HttpClient;
+
+@SuppressWarnings("java:S2187")
+public class VertxHttpLoggingInterceptorTest extends AbstractHttpLoggingInterceptorTest {
+
+ @Override
+ protected HttpClient.Factory getHttpClientFactory() {
+ return new VertxHttpClientFactory();
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpRequestTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpRequestTest.java
new file mode 100644
index 00000000000..1b95ec6e25b
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxHttpRequestTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.AsyncBody;
+import io.fabric8.kubernetes.client.http.HttpResponse;
+import io.fabric8.kubernetes.client.http.StandardHttpRequest;
+import io.fabric8.kubernetes.client.http.StandardHttpRequest.BodyContent;
+import io.fabric8.kubernetes.client.http.StandardHttpRequest.ByteArrayBodyContent;
+import io.fabric8.kubernetes.client.http.StandardHttpRequest.InputStreamBodyContent;
+import io.fabric8.kubernetes.client.http.StandardHttpRequest.StringBodyContent;
+import io.vertx.core.Future;
+import io.vertx.core.MultiMap;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import io.vertx.core.http.RequestOptions;
+import io.vertx.core.streams.ReadStream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@DisplayName("VertxHttpRequest")
+class VertxHttpRequestTest {
+
+ @Mock
+ private Vertx vertx;
+
+ @Mock
+ private RequestOptions options;
+
+ @Mock
+ private StandardHttpRequest request;
+
+ @Mock
+ private HttpClient httpClient;
+
+ @Mock
+ private HttpClientRequest httpClientRequest;
+
+ @Mock
+ private HttpClientResponse httpClientResponse;
+
+ @Mock
+ private AsyncBody.Consumer> consumer;
+
+ @Mock
+ private StringBodyContent stringBodyContent;
+
+ @Mock
+ private ByteArrayBodyContent byteArrayBodyContent;
+
+ @Mock
+ private InputStreamBodyContent inputStreamBodyContent;
+
+ @Mock
+ private BodyContent unsupportedBodyContent;
+
+ @Mock
+ private MultiMap multiMap;
+
+ @Captor
+ private ArgumentCaptor bufferCaptor;
+
+ @Captor
+ private ArgumentCaptor> readStreamCaptor;
+
+ private VertxHttpRequest vertxHttpRequest;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ vertxHttpRequest = new VertxHttpRequest(vertx, options, request);
+
+ when(httpClientResponse.handler(any())).thenReturn(httpClientResponse);
+ when(httpClientResponse.endHandler(any())).thenReturn(httpClientResponse);
+ when(httpClientResponse.exceptionHandler(any())).thenReturn(httpClientResponse);
+
+ when(httpClientResponse.headers()).thenReturn(multiMap);
+ when(multiMap.names()).thenReturn(java.util.Set.of());
+ }
+
+ @Test
+ @DisplayName("Should execute request flow and return CompletableFuture")
+ void consumeBytes_shouldExecuteRequestFlowSuccessfully() throws Exception {
+ when(httpClient.request(options)).thenReturn(Future.succeededFuture(httpClientRequest));
+ when(request.body()).thenReturn(null);
+ when(httpClientRequest.send()).thenReturn(Future.succeededFuture(httpClientResponse));
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ HttpResponse response = result.get(1, TimeUnit.SECONDS);
+ assertThat(response).isNotNull().isInstanceOf(VertxHttpResponse.class);
+
+ verify(httpClientResponse).pause();
+ }
+
+ @Test
+ @DisplayName("Should handle failed client request")
+ void consumeBytes_shouldHandleFailedClientRequest() {
+ RuntimeException clientException = new RuntimeException("Client request failed");
+ when(httpClient.request(options)).thenReturn(Future.failedFuture(clientException));
+ when(request.body()).thenReturn(null);
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ assertThat(result).isCompletedExceptionally();
+ assertThatThrownBy(() -> result.get(1, TimeUnit.SECONDS))
+ .isInstanceOf(ExecutionException.class)
+ .hasCause(clientException);
+ }
+
+ @Test
+ @DisplayName("Should send request without body when body is null")
+ void sendBody_shouldSendWithoutBodyWhenBodyIsNull() throws Exception {
+ when(httpClient.request(options)).thenReturn(Future.succeededFuture(httpClientRequest));
+ when(request.body()).thenReturn(null);
+ when(httpClientRequest.send()).thenReturn(Future.succeededFuture(httpClientResponse));
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ HttpResponse response = result.get(1, TimeUnit.SECONDS);
+ assertThat(response).isNotNull();
+ verify(httpClientRequest).send();
+ }
+
+ @Test
+ @DisplayName("Should send string body content as Buffer")
+ void sendBody_shouldSendStringBodyContentAsBuffer() throws Exception {
+ String content = "test string content";
+ when(httpClient.request(options)).thenReturn(Future.succeededFuture(httpClientRequest));
+ when(request.body()).thenReturn(stringBodyContent);
+ when(stringBodyContent.getContent()).thenReturn(content);
+ when(httpClientRequest.send(any(Buffer.class))).thenReturn(Future.succeededFuture(httpClientResponse));
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ HttpResponse response = result.get(1, TimeUnit.SECONDS);
+ assertThat(response).isNotNull();
+ verify(httpClientRequest).send(bufferCaptor.capture());
+
+ Buffer capturedBuffer = bufferCaptor.getValue();
+ assertThat(capturedBuffer).hasToString(content);
+ }
+
+ @Test
+ @DisplayName("Should send byte array body content as Buffer")
+ void sendBody_shouldSendByteArrayBodyContentAsBuffer() throws Exception {
+ byte[] content = "test byte array".getBytes();
+ when(httpClient.request(options)).thenReturn(Future.succeededFuture(httpClientRequest));
+ when(request.body()).thenReturn(byteArrayBodyContent);
+ when(byteArrayBodyContent.getContent()).thenReturn(content);
+ when(httpClientRequest.send(any(Buffer.class))).thenReturn(Future.succeededFuture(httpClientResponse));
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ HttpResponse response = result.get(1, TimeUnit.SECONDS);
+ assertThat(response).isNotNull();
+ verify(httpClientRequest).send(bufferCaptor.capture());
+
+ Buffer capturedBuffer = bufferCaptor.getValue();
+ assertThat(capturedBuffer.getBytes()).isEqualTo(content);
+ }
+
+ @Test
+ @DisplayName("Should send InputStream body content as ReadStream")
+ void sendBody_shouldSendInputStreamBodyContentAsReadStream() throws Exception {
+ byte[] content = "test input stream".getBytes();
+ InputStream inputStream = new ByteArrayInputStream(content);
+ when(httpClient.request(options)).thenReturn(Future.succeededFuture(httpClientRequest));
+ when(request.body()).thenReturn(inputStreamBodyContent);
+ when(inputStreamBodyContent.getContent()).thenReturn(inputStream);
+ when(httpClientRequest.send(any(ReadStream.class))).thenReturn(Future.succeededFuture(httpClientResponse));
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ HttpResponse response = result.get(1, TimeUnit.SECONDS);
+ assertThat(response).isNotNull();
+ verify(httpClientRequest).send(readStreamCaptor.capture());
+
+ ReadStream capturedStream = readStreamCaptor.getValue();
+ assertThat(capturedStream).isInstanceOf(InputStreamReadStream.class);
+ }
+
+ @Test
+ @DisplayName("Should return failed future for unsupported body content type")
+ void sendBody_shouldReturnFailedFutureForUnsupportedBodyContent() {
+ when(httpClient.request(options)).thenReturn(Future.succeededFuture(httpClientRequest));
+ when(request.body()).thenReturn(unsupportedBodyContent);
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ assertThat(result).isCompletedExceptionally();
+ assertThatThrownBy(() -> result.get(1, TimeUnit.SECONDS))
+ .isInstanceOf(ExecutionException.class)
+ .hasRootCauseInstanceOf(IllegalArgumentException.class)
+ .hasRootCauseMessage("Unsupported body content: " + unsupportedBodyContent.getClass());
+ }
+
+ @Test
+ @DisplayName("Should create VertxHttpResponse with paused response and VertxAsyncBody")
+ void toFabricResponse_shouldCreateResponseWithPausedResponseAndAsyncBody() throws Exception {
+ when(httpClient.request(options)).thenReturn(Future.succeededFuture(httpClientRequest));
+ when(request.body()).thenReturn(null);
+ when(httpClientRequest.send()).thenReturn(Future.succeededFuture(httpClientResponse));
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ HttpResponse response = result.get(1, TimeUnit.SECONDS);
+ assertThat(response).isInstanceOf(VertxHttpResponse.class);
+ verify(httpClientResponse).pause();
+
+ VertxHttpResponse vertxResponse = (VertxHttpResponse) response;
+ assertThat(vertxResponse.body()).isInstanceOf(VertxAsyncBody.class);
+ }
+
+ @Test
+ @DisplayName("Should convert MultiMap to headers map correctly")
+ void toHeadersMap_shouldConvertMultiMapToHeadersMap() {
+ when(multiMap.names()).thenReturn(java.util.Set.of("Content-Type", "Authorization", "Custom-Header"));
+ when(multiMap.getAll("Content-Type")).thenReturn(List.of("application/json"));
+ when(multiMap.getAll("Authorization")).thenReturn(List.of("Bearer token123"));
+ when(multiMap.getAll("Custom-Header")).thenReturn(List.of("value1", "value2"));
+
+ Map> result = VertxHttpRequest.toHeadersMap(multiMap);
+
+ assertThat(result).hasSize(3);
+ assertThat(result.get("Content-Type")).containsExactly("application/json");
+ assertThat(result.get("Authorization")).containsExactly("Bearer token123");
+ assertThat(result.get("Custom-Header")).containsExactly("value1", "value2");
+ }
+
+ @Test
+ @DisplayName("Should handle empty MultiMap correctly")
+ void toHeadersMap_shouldHandleEmptyMultiMap() {
+ when(multiMap.names()).thenReturn(java.util.Set.of());
+
+ Map> result = VertxHttpRequest.toHeadersMap(multiMap);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ @DisplayName("Should preserve header order using LinkedHashMap")
+ void toHeadersMap_shouldPreserveHeaderOrderUsingLinkedHashMap() {
+ java.util.LinkedHashSet orderedNames = new java.util.LinkedHashSet<>();
+ orderedNames.add("First");
+ orderedNames.add("Second");
+ orderedNames.add("Third");
+ when(multiMap.names()).thenReturn(orderedNames);
+ when(multiMap.getAll("First")).thenReturn(List.of("1"));
+ when(multiMap.getAll("Second")).thenReturn(List.of("2"));
+ when(multiMap.getAll("Third")).thenReturn(List.of("3"));
+
+ Map> result = VertxHttpRequest.toHeadersMap(multiMap);
+
+ String[] headerNames = result.keySet().toArray(new String[0]);
+ assertThat(headerNames).containsExactly("First", "Second", "Third");
+ }
+
+ @Test
+ @DisplayName("Should handle single header with multiple values correctly")
+ void toHeadersMap_shouldHandleSingleHeaderWithMultipleValues() {
+ when(multiMap.names()).thenReturn(java.util.Set.of("Accept"));
+ when(multiMap.getAll("Accept")).thenReturn(List.of("application/json", "text/plain", "*/*"));
+
+ Map> result = VertxHttpRequest.toHeadersMap(multiMap);
+
+ assertThat(result).hasSize(1);
+ assertThat(result.get("Accept")).containsExactly("application/json", "text/plain", "*/*");
+ }
+
+ @Test
+ @DisplayName("Should handle send body failure in consumeBytes")
+ void consumeBytes_shouldHandleSendBodyFailure() {
+ RuntimeException sendBodyException = new RuntimeException("Send body failed");
+ when(httpClient.request(options)).thenReturn(Future.succeededFuture(httpClientRequest));
+ when(request.body()).thenReturn(stringBodyContent);
+ when(stringBodyContent.getContent()).thenReturn("test");
+ when(httpClientRequest.send(any(Buffer.class))).thenReturn(Future.failedFuture(sendBodyException));
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ assertThat(result).isCompletedExceptionally();
+ assertThatThrownBy(() -> result.get(1, TimeUnit.SECONDS))
+ .isInstanceOf(ExecutionException.class)
+ .hasCause(sendBodyException);
+ }
+
+ @Test
+ @DisplayName("Should handle response mapping in consumeBytes")
+ void consumeBytes_shouldHandleResponseMapping() throws Exception {
+ when(httpClient.request(options)).thenReturn(Future.succeededFuture(httpClientRequest));
+ when(request.body()).thenReturn(stringBodyContent);
+ when(stringBodyContent.getContent()).thenReturn("test content");
+ when(httpClientRequest.send(any(Buffer.class))).thenReturn(Future.succeededFuture(httpClientResponse));
+
+ CompletableFuture> result = vertxHttpRequest.consumeBytes(httpClient, consumer);
+
+ HttpResponse response = result.get(1, TimeUnit.SECONDS);
+ assertThat(response).isNotNull().isInstanceOf(VertxHttpResponse.class);
+
+ verify(httpClientResponse).pause();
+ verify(httpClientRequest).send(any(Buffer.class));
+ }
+}
diff --git a/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxWebSocketTest.java b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxWebSocketTest.java
new file mode 100644
index 00000000000..b88f01dc74d
--- /dev/null
+++ b/httpclient-vertx-5/src/test/java/io/fabric8/kubernetes/client/vertx/VertxWebSocketTest.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed 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 io.fabric8.kubernetes.client.vertx;
+
+import io.fabric8.kubernetes.client.http.WebSocket;
+import io.netty.handler.codec.http.websocketx.CorruptedWebSocketFrameException;
+import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Promise;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.http.HttpClosedException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.nio.ByteBuffer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@DisplayName("VertxWebSocket")
+class VertxWebSocketTest {
+
+ @Mock
+ private io.vertx.core.http.WebSocket vertxWebSocket;
+
+ @Mock
+ private WebSocket.Listener listener;
+
+ @Captor
+ private ArgumentCaptor> binaryMessageHandlerCaptor;
+
+ @Captor
+ private ArgumentCaptor> textMessageHandlerCaptor;
+
+ @Captor
+ private ArgumentCaptor> endHandlerCaptor;
+
+ @Captor
+ private ArgumentCaptor> exceptionHandlerCaptor;
+
+ private VertxWebSocket webSocket;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+
+ // Setup mock chaining for handler registration
+ when(vertxWebSocket.binaryMessageHandler(binaryMessageHandlerCaptor.capture())).thenReturn(vertxWebSocket);
+ when(vertxWebSocket.textMessageHandler(textMessageHandlerCaptor.capture())).thenReturn(vertxWebSocket);
+ when(vertxWebSocket.endHandler(endHandlerCaptor.capture())).thenReturn(vertxWebSocket);
+ when(vertxWebSocket.exceptionHandler(exceptionHandlerCaptor.capture())).thenReturn(vertxWebSocket);
+
+ webSocket = new VertxWebSocket(vertxWebSocket, listener);
+ }
+
+ @Test
+ @DisplayName("Should setup event handlers and notify listener on initHandlers")
+ void initHandlers_shouldSetupHandlersAndNotifyListener() {
+ webSocket.initHandlers();
+
+ verify(vertxWebSocket).binaryMessageHandler(any());
+ verify(vertxWebSocket).textMessageHandler(any());
+ verify(vertxWebSocket).endHandler(any());
+ verify(vertxWebSocket).exceptionHandler(any());
+ verify(listener).onOpen(webSocket);
+
+ assertThat(binaryMessageHandlerCaptor.getValue()).isNotNull();
+ assertThat(textMessageHandlerCaptor.getValue()).isNotNull();
+ assertThat(endHandlerCaptor.getValue()).isNotNull();
+ assertThat(exceptionHandlerCaptor.getValue()).isNotNull();
+ }
+
+ @Test
+ @DisplayName("Should handle binary message with pause and listener callback")
+ void binaryMessageHandler_shouldHandleBinaryMessageWithBackpressure() {
+ webSocket.initHandlers();
+
+ byte[] testData = "binary test data".getBytes();
+ Buffer buffer = Buffer.buffer(testData);
+
+ binaryMessageHandlerCaptor.getValue().handle(buffer);
+
+ verify(vertxWebSocket).pause();
+
+ ArgumentCaptor byteBufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
+ verify(listener).onMessage(eq(webSocket), byteBufferCaptor.capture());
+
+ ByteBuffer capturedBuffer = byteBufferCaptor.getValue();
+ byte[] capturedBytes = new byte[capturedBuffer.remaining()];
+ capturedBuffer.get(capturedBytes);
+ assertThat(capturedBytes).isEqualTo(testData);
+ }
+
+ @Test
+ @DisplayName("Should handle text message with pause and listener callback")
+ void textMessageHandler_shouldHandleTextMessageWithBackpressure() {
+ webSocket.initHandlers();
+
+ String testMessage = "text test message";
+
+ textMessageHandlerCaptor.getValue().handle(testMessage);
+
+ verify(vertxWebSocket).pause();
+ verify(listener).onMessage(webSocket, testMessage);
+ }
+
+ @Test
+ @DisplayName("Should handle connection end with close status and reason")
+ void endHandler_shouldHandleConnectionEndWithStatusAndReason() {
+ webSocket.initHandlers();
+
+ short closeCode = 1000;
+ String closeReason = "Normal closure";
+ when(vertxWebSocket.closeStatusCode()).thenReturn(closeCode);
+ when(vertxWebSocket.closeReason()).thenReturn(closeReason);
+
+ endHandlerCaptor.getValue().handle(null);
+
+ verify(listener).onClose(webSocket, closeCode, closeReason);
+ }
+
+ @Test
+ @DisplayName("Should handle exception with error mapping and connection cleanup")
+ void exceptionHandler_shouldHandleExceptionWithMappingAndCleanup() {
+ webSocket.initHandlers();
+
+ RuntimeException originalError = new RuntimeException("Connection error");
+ when(vertxWebSocket.isClosed()).thenReturn(false);
+
+ exceptionHandlerCaptor.getValue().handle(originalError);
+
+ verify(listener).onError(webSocket, originalError);
+ verify(vertxWebSocket).close();
+ }
+
+ @Test
+ @DisplayName("Should not close already closed socket in exception handler")
+ void exceptionHandler_shouldNotCloseAlreadyClosedSocket() {
+ webSocket.initHandlers();
+
+ RuntimeException originalError = new RuntimeException("Connection error");
+ when(vertxWebSocket.isClosed()).thenReturn(true);
+
+ exceptionHandlerCaptor.getValue().handle(originalError);
+
+ verify(listener).onError(webSocket, originalError);
+ verify(vertxWebSocket, never()).close();
+ }
+
+ @Test
+ @DisplayName("Should map CorruptedWebSocketFrameException to ProtocolException")
+ void exceptionHandler_shouldMapCorruptedFrameException() {
+ webSocket.initHandlers();
+
+ var corruptedFrame = new CorruptedWebSocketFrameException(WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "Corrupted frame",
+ null);
+ when(vertxWebSocket.isClosed()).thenReturn(false);
+
+ exceptionHandlerCaptor.getValue().handle(corruptedFrame);
+
+ ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Throwable.class);
+ verify(listener).onError(eq(webSocket), errorCaptor.capture());
+
+ Throwable mappedException = errorCaptor.getValue();
+ assertThat(mappedException).isInstanceOf(ProtocolException.class);
+ assertThat(mappedException.getCause()).isSameAs(corruptedFrame);
+ assertThat(mappedException.getMessage()).isEqualTo("Corrupted frame");
+ }
+
+ @Test
+ @DisplayName("Should map HttpClosedException to IOException")
+ void exceptionHandler_shouldMapHttpClosedException() {
+ webSocket.initHandlers();
+
+ HttpClosedException httpClosed = new HttpClosedException("Connection closed");
+ when(vertxWebSocket.isClosed()).thenReturn(false);
+
+ exceptionHandlerCaptor.getValue().handle(httpClosed);
+
+ ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(Throwable.class);
+ verify(listener).onError(eq(webSocket), errorCaptor.capture());
+
+ Throwable mappedException = errorCaptor.getValue();
+ assertThat(mappedException).isInstanceOf(IOException.class);
+ assertThat(mappedException.getCause()).isSameAs(httpClosed);
+ assertThat(mappedException.getMessage()).isEqualTo("WebSocket connection closed unexpectedly");
+ }
+
+ @Test
+ @DisplayName("Should send array-backed ByteBuffer with zero-copy optimization")
+ void send_shouldSendArrayBackedByteBufferWithZeroCopy() {
+ byte[] testData = "test data for array buffer".getBytes();
+ ByteBuffer buffer = ByteBuffer.wrap(testData);
+
+ Future successfulWrite = Future.succeededFuture();
+ when(vertxWebSocket.writeBinaryMessage(any(Buffer.class))).thenReturn(successfulWrite);
+
+ boolean result = webSocket.send(buffer);
+
+ assertThat(result).isTrue();
+ assertThat(buffer.position()).isEqualTo(buffer.limit()); // Buffer consumed
+ assertThat(webSocket.queueSize()).isZero(); // Synchronous completion
+
+ ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(Buffer.class);
+ verify(vertxWebSocket).writeBinaryMessage(bufferCaptor.capture());
+
+ Buffer sentBuffer = bufferCaptor.getValue();
+ assertThat(sentBuffer.getBytes()).isEqualTo(testData);
+ }
+
+ @Test
+ @DisplayName("Should send direct ByteBuffer with array copy")
+ void send_shouldSendDirectByteBufferWithCopy() {
+ ByteBuffer buffer = ByteBuffer.allocateDirect(16);
+ buffer.put("direct buffer".getBytes());
+ buffer.flip();
+
+ Future successfulWrite = Future.succeededFuture();
+ when(vertxWebSocket.writeBinaryMessage(any(Buffer.class))).thenReturn(successfulWrite);
+
+ boolean result = webSocket.send(buffer);
+
+ assertThat(result).isTrue();
+ assertThat(buffer.position()).isEqualTo(buffer.limit()); // Buffer consumed
+ assertThat(webSocket.queueSize()).isZero(); // Synchronous completion
+
+ verify(vertxWebSocket).writeBinaryMessage(any(Buffer.class));
+ }
+
+ @Test
+ @DisplayName("Should handle immediate send failure")
+ void send_shouldHandleImmediateSendFailure() {
+ ByteBuffer buffer = ByteBuffer.wrap("test data".getBytes());
+
+ RuntimeException writeError = new RuntimeException("Write failed");
+ Future failedWrite = Future.failedFuture(writeError);
+ when(vertxWebSocket.writeBinaryMessage(any(Buffer.class))).thenReturn(failedWrite);
+
+ boolean result = webSocket.send(buffer);
+
+ assertThat(result).isFalse();
+ assertThat(webSocket.queueSize()).isZero(); // Should not track failed writes
+ }
+
+ @Test
+ @DisplayName("Should handle asynchronous send completion")
+ void send_shouldHandleAsynchronousSendCompletion() {
+ ByteBuffer buffer = ByteBuffer.wrap("test data".getBytes());
+ int expectedLength = buffer.remaining();
+
+ Promise promise = Promise.promise();
+ when(vertxWebSocket.writeBinaryMessage(any(Buffer.class))).thenReturn(promise.future());
+
+ boolean result = webSocket.send(buffer);
+
+ assertThat(result).isTrue();
+ assertThat(webSocket.queueSize()).isEqualTo(expectedLength); // Should track pending write
+
+ promise.tryComplete();
+
+ assertThat(webSocket.queueSize()).isZero(); // Should clear pending count
+ }
+
+ @Test
+ @DisplayName("Should handle asynchronous send failure")
+ void send_shouldHandleAsynchronousSendFailure() {
+ ByteBuffer buffer = ByteBuffer.wrap("test data".getBytes());
+ int expectedLength = buffer.remaining();
+
+ Promise writePromise = Promise.promise();
+ when(vertxWebSocket.writeBinaryMessage(any(Buffer.class))).thenReturn(writePromise.future());
+
+ boolean result = webSocket.send(buffer);
+
+ // Should accept the send and track bytes
+ assertThat(result).isTrue();
+ assertThat(webSocket.queueSize()).isEqualTo(expectedLength);
+
+ // Fail the async operation
+ writePromise.tryFail("Async write failed");
+
+ // Pending counter must be decremented
+ assertThat(webSocket.queueSize()).isZero();
+ }
+
+ @Test
+ @DisplayName("Should track queue size correctly across multiple sends")
+ void queueSize_shouldTrackMultiplePendingOperations() {
+ ByteBuffer buffer1 = ByteBuffer.wrap("first message".getBytes());
+ ByteBuffer buffer2 = ByteBuffer.wrap("second message".getBytes());
+
+ Promise writePromise1 = Promise.promise();
+ Promise writePromise2 = Promise.promise();
+ when(vertxWebSocket.writeBinaryMessage(any(Buffer.class)))
+ .thenReturn(writePromise1.future())
+ .thenReturn(writePromise2.future());
+
+ // Send the first message
+ webSocket.send(buffer1);
+ int queueAfterFirst = (int) webSocket.queueSize();
+
+ // Send the second message
+ webSocket.send(buffer2);
+ int queueAfterSecond = (int) webSocket.queueSize();
+
+ assertThat(queueAfterFirst).isEqualTo("first message".length());
+ assertThat(queueAfterSecond).isEqualTo("first message".length() + "second message".length());
+
+ // Complete first async write
+ writePromise1.tryComplete();
+ assertThat(webSocket.queueSize()).isEqualTo("second message".length());
+
+ // Complete second async write
+ writePromise2.tryComplete();
+ assertThat(webSocket.queueSize()).isZero();
+ }
+
+ @Test
+ @DisplayName("Should send close frame and fetch final frame when not closed")
+ void sendClose_shouldSendCloseFrameAndFetchFinalFrame() {
+ int closeCode = 1000;
+ String closeReason = "Normal closure";
+
+ when(vertxWebSocket.isClosed()).thenReturn(false);
+ when(vertxWebSocket.close((short) closeCode, closeReason)).thenReturn(Future.succeededFuture());
+
+ boolean result = webSocket.sendClose(closeCode, closeReason);
+
+ assertThat(result).isTrue();
+ verify(vertxWebSocket).close((short) closeCode, closeReason);
+ verify(vertxWebSocket).fetch(1);
+ }
+
+ @Test
+ @DisplayName("Should return false when trying to close already closed socket")
+ void sendClose_shouldReturnFalseWhenAlreadyClosed() {
+ int closeCode = 1000;
+ String closeReason = "Normal closure";
+
+ when(vertxWebSocket.isClosed()).thenReturn(true);
+
+ boolean result = webSocket.sendClose(closeCode, closeReason);
+
+ assertThat(result).isFalse();
+ verify(vertxWebSocket, never()).close(any(Short.class), any(String.class));
+ verify(vertxWebSocket, never()).fetch(1);
+ }
+
+ @Test
+ @DisplayName("Should handle close operation failure gracefully")
+ void sendClose_shouldHandleCloseOperationFailureGracefully() {
+ int closeCode = 1000;
+ String closeReason = "Normal closure";
+
+ when(vertxWebSocket.isClosed()).thenReturn(false);
+ when(vertxWebSocket.close((short) closeCode, closeReason))
+ .thenReturn(Future.failedFuture("Close failed"));
+
+ boolean result = webSocket.sendClose(closeCode, closeReason);
+
+ assertThat(result).isTrue(); // Should still return true for attempted close
+ verify(vertxWebSocket).close((short) closeCode, closeReason);
+ verify(vertxWebSocket).fetch(1);
+ }
+
+ @Test
+ @DisplayName("Should request next frame by calling fetch")
+ void request_shouldCallFetchOnVertxWebSocket() {
+ webSocket.request();
+
+ verify(vertxWebSocket).fetch(1);
+ }
+
+ @Test
+ @DisplayName("Should maintain backpressure protocol across message handling")
+ void backpressureProtocol_shouldMaintainFlowControl() {
+ webSocket.initHandlers();
+
+ var inOrder = inOrder(vertxWebSocket);
+
+ // Send first message
+ binaryMessageHandlerCaptor.getValue().handle(Buffer.buffer("message1"));
+ // After a frame is delivered, Vert.x socket should pause
+ inOrder.verify(vertxWebSocket).pause();
+
+ // Application requests next frame
+ webSocket.request();
+ inOrder.verify(vertxWebSocket).fetch(1);
+
+ // Send the second message
+ textMessageHandlerCaptor.getValue().handle("message2");
+
+ // The socket should pause again after delivering the second frame
+ inOrder.verify(vertxWebSocket).pause();
+
+ // No further interactions expected
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @DisplayName("Should return zero queue size initially")
+ void queueSize_shouldReturnZeroInitially() {
+ assertThat(webSocket.queueSize()).isZero();
+ }
+}
diff --git a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/vertx/ServerWebSocketHandler.java b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/vertx/ServerWebSocketHandler.java
index ceec5a94fdf..61644905716 100644
--- a/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/vertx/ServerWebSocketHandler.java
+++ b/junit/mockwebserver/src/main/java/io/fabric8/mockwebserver/vertx/ServerWebSocketHandler.java
@@ -37,10 +37,23 @@ public ServerWebSocketHandler(RecordedRequest request, Response response) {
public void handle(ServerWebSocket serverWebSocket) {
final WebSocketListener wsListener = response.getWebSocketListener();
final VertxMockWebSocket mockWebSocket = new VertxMockWebSocket(request, serverWebSocket);
- // Important to call onBeforeAccept before sending accept so that WebSockets get registered by dispatchers, handlers, and so on
+ // Important to call onBeforeAccept before configuring so that WebSockets get registered by dispatchers, handlers, and so on
wsListener.onBeforeAccept(mockWebSocket, response);
- serverWebSocket.textMessageHandler(text -> wsListener.onMessage(mockWebSocket, text));
- serverWebSocket.binaryMessageHandler(buff -> wsListener.onMessage(mockWebSocket, buff.getBytes()));
+ configureWebSocket(serverWebSocket, mockWebSocket, wsListener);
+ wsListener.onOpen(mockWebSocket, response);
+ serverWebSocket.fetch(1);
+ }
+
+ public void configureWebSocket(ServerWebSocket serverWebSocket, VertxMockWebSocket mockWebSocket,
+ WebSocketListener wsListener) {
+ serverWebSocket.textMessageHandler(text -> {
+ wsListener.onMessage(mockWebSocket, text);
+ serverWebSocket.fetch(1);
+ });
+ serverWebSocket.binaryMessageHandler(buff -> {
+ wsListener.onMessage(mockWebSocket, buff.getBytes());
+ serverWebSocket.fetch(1);
+ });
serverWebSocket.frameHandler(frame -> {
if (frame.isClose()) {
wsListener.onClosing(mockWebSocket, frame.closeStatusCode(), frame.closeReason());
@@ -54,8 +67,5 @@ public void handle(ServerWebSocket serverWebSocket) {
: serverWebSocket.closeStatusCode(),
serverWebSocket.closeReason()));
serverWebSocket.exceptionHandler(err -> wsListener.onFailure(mockWebSocket, err, response));
- serverWebSocket.accept();
- wsListener.onOpen(mockWebSocket, response);
- serverWebSocket.fetch(1);
}
}
diff --git a/junit/mockwebserver/src/test/groovy/io/fabric8/mockwebserver/DefaultMockServerTest.groovy b/junit/mockwebserver/src/test/groovy/io/fabric8/mockwebserver/DefaultMockServerTest.groovy
index 4e8b5b64245..12623cd196d 100644
--- a/junit/mockwebserver/src/test/groovy/io/fabric8/mockwebserver/DefaultMockServerTest.groovy
+++ b/junit/mockwebserver/src/test/groovy/io/fabric8/mockwebserver/DefaultMockServerTest.groovy
@@ -346,12 +346,12 @@ class DefaultMockServerTest extends Specification {
and: "A WebSocket request"
def wsReq =wsClient.webSocket().connect(server.port, server.getHostName(), "/api/v1/users/watch")
and: "A WebSocket listener"
- wsReq.onComplete { ws ->
- ws.result().textMessageHandler { text ->
+ wsReq.onSuccess { ws ->
+ ws.textMessageHandler { text ->
receivedMessages.add(text)
}
- ws.result().closeHandler { _ ->
- ws.result().close()
+ ws.closeHandler { _ ->
+ ws.close()
}
}
and: "An instance of PollingConditions"
@@ -385,12 +385,12 @@ class DefaultMockServerTest extends Specification {
and: "A WebSocket request"
def wsReq = wsClient.webSocket().connect(server.port, server.getHostName(), "/api/v1/users/watch")
and: "A WebSocket listener"
- wsReq.onComplete { ws ->
- ws.result().binaryMessageHandler { buffer ->
+ wsReq.onSuccess { ws ->
+ ws.binaryMessageHandler { buffer ->
receivedMessages.add(buffer.getBytes(0, buffer.length()))
}
- ws.result().closeHandler { _ ->
- ws.result().close()
+ ws.closeHandler { _ ->
+ ws.close()
}
}
and: "An instance of PollingConditions"
@@ -421,12 +421,12 @@ class DefaultMockServerTest extends Specification {
and: "A WebSocket request"
def wsReq = wsClient.webSocket().connect(server.port, server.getHostName(), "/api/v1/users/watch")
and: "A WebSocket listener"
- wsReq.onComplete { ws ->
- ws.result().textMessageHandler { text ->
+ wsReq.onSuccess { ws ->
+ ws.textMessageHandler { text ->
receivedMessages.add(text)
}
- ws.result().writeTextMessage("create root")
- ws.result().writeTextMessage("delete root")
+ ws.writeTextMessage("create root")
+ ws.writeTextMessage("delete root")
}
and: "An instance of PollingConditions"
def conditions = new PollingConditions(timeout: 10)
@@ -452,8 +452,8 @@ class DefaultMockServerTest extends Specification {
and: "A WebSocket request"
def wsReq = wsClient.webSocket().connect(server.port, server.getHostName(), "/api/v1/users/watch")
and: "A WebSocket listener"
- wsReq.onComplete { ws ->
- ws.result().writeTextMessage("unexpected message")
+ wsReq.onSuccess { ws ->
+ ws.writeTextMessage("unexpected message")
}
and: "An instance of PollingConditions"
def conditions = new PollingConditions(timeout: 10)
@@ -498,7 +498,7 @@ class DefaultMockServerTest extends Specification {
def "when using a body provider it should work as for static responses"() {
given: "A counter"
- def counter = new AtomicInteger(0);
+ def counter = new AtomicInteger(0)
and: "An expectation with body provider"
server.expect().get().withPath("/api/v1/users")
.andReply(200, {req -> "admin-" + counter.getAndIncrement()})
@@ -529,7 +529,7 @@ class DefaultMockServerTest extends Specification {
given: "An expectation with response provider"
server.expect().get().withPath("/api/v1/users")
.andReply(new ResponseProvider