This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new c2711b80d5 Next Generation RestClient
c2711b80d5 is described below

commit c2711b80d52b47e9735710557d3864e05da6d365
Author: James Bognar <[email protected]>
AuthorDate: Thu Apr 9 16:05:05 2026 -0400

    Next Generation RestClient
---
 juneau-rest/juneau-ng-rest-client-jetty/pom.xml    |  88 +++++++
 .../ng/rest/client/jetty/JettyHttpTransport.java   | 161 ++++++++++++
 .../client/jetty/JettyHttpTransportBuilder.java    |  89 +++++++
 .../client/jetty/JettyHttpTransportProvider.java   |  65 +++++
 ...che.juneau.ng.rest.client.HttpTransportProvider |  16 ++
 juneau-rest/juneau-ng-rest-client-okhttp/pom.xml   |  88 +++++++
 .../ng/rest/client/okhttp/OkHttpTransport.java     | 164 ++++++++++++
 .../rest/client/okhttp/OkHttpTransportBuilder.java |  72 ++++++
 .../client/okhttp/OkHttpTransportProvider.java     |  61 +++++
 ...che.juneau.ng.rest.client.HttpTransportProvider |  16 ++
 juneau-rest/pom.xml                                |   2 +
 juneau-utest/pom.xml                               |  10 +
 .../juneau/ng/rest/JettyHttpTransport_Test.java    | 275 ++++++++++++++++++++
 .../juneau/ng/rest/OkHttpTransport_Test.java       | 276 +++++++++++++++++++++
 14 files changed, 1383 insertions(+)

diff --git a/juneau-rest/juneau-ng-rest-client-jetty/pom.xml 
b/juneau-rest/juneau-ng-rest-client-jetty/pom.xml
new file mode 100644
index 0000000000..9f39ae0c73
--- /dev/null
+++ b/juneau-rest/juneau-ng-rest-client-jetty/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+
+       <modelVersion>4.0.0</modelVersion>
+
+       <parent>
+               <groupId>org.apache.juneau</groupId>
+               <artifactId>juneau-rest</artifactId>
+               <version>9.2.1-SNAPSHOT</version>
+       </parent>
+
+       <artifactId>juneau-ng-rest-client-jetty</artifactId>
+       <name>Apache Juneau NG REST Client — Jetty HttpClient Transport</name>
+       <description>Jetty 12 HttpClient transport adapter for the 
next-generation Juneau REST client.</description>
+       <packaging>bundle</packaging>
+
+       <properties>
+               
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+       </properties>
+
+       <dependencies>
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-rest-client</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.eclipse.jetty</groupId>
+                       <artifactId>jetty-client</artifactId>
+                       <version>${jetty.version}</version>
+               </dependency>
+       </dependencies>
+
+       <build>
+               <plugins>
+                       <plugin>
+                               <groupId>org.apache.felix</groupId>
+                               <artifactId>maven-bundle-plugin</artifactId>
+                               <extensions>true</extensions>
+                               <configuration>
+                                       
<supportIncrementalBuild>true</supportIncrementalBuild>
+                               </configuration>
+                               <executions>
+                               <execution>
+                                       <id>bundle-manifest</id>
+                                       <phase>process-classes</phase>
+                                       <goals>
+                                               <goal>manifest</goal>
+                                       </goals>
+                               </execution>
+                       </executions>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-source-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <id>attach-sources</id>
+                                               <phase>verify</phase>
+                                               <goals>
+                                                       <goal>jar-no-fork</goal>
+                                               </goals>
+                                       </execution>
+                               </executions>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-jar-plugin</artifactId>
+                       </plugin>
+               </plugins>
+       </build>
+</project>
diff --git 
a/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransport.java
 
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransport.java
new file mode 100644
index 0000000000..543feeb073
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransport.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.rest.client.jetty;
+
+import java.io.*;
+import java.util.concurrent.*;
+
+import org.eclipse.jetty.client.*;
+import org.eclipse.jetty.http.*;
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransport} implementation backed by Jetty 12 {@link HttpClient}.
+ *
+ * <p>
+ * This transport is auto-discovered via {@link java.util.ServiceLoader} when
+ * {@code org.eclipse.jetty:jetty-client} is on the classpath.  You can also 
instantiate it explicitly:
+ *
+ * <p class='bjava'>
+ *     <jv>transport</jv> = JettyHttpTransport.<jsm>builder</jsm>()
+ *             .httpClient(<jk>new</jk> HttpClient())
+ *             .build();
+ *
+ *     <jv>client</jv> = NgRestClient.<jsm>builder</jsm>()
+ *             .transport(<jv>transport</jv>)
+ *             .build();
+ * </p>
+ *
+ * <p>
+ * The Jetty {@link HttpClient} is started automatically when the transport is 
created and stopped when
+ * {@link #close()} is called.  The request body is streamed via {@link 
OutputStreamRequestContent} using
+ * a background thread, avoiding full in-memory buffering.  The response 
header timeout defaults to 30 seconds
+ * and is configurable via {@link 
JettyHttpTransportBuilder#responseTimeoutMs(long)}.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the 
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in 
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client";>juneau-ng 
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class JettyHttpTransport implements HttpTransport {
+
+       private final HttpClient httpClient;
+       private final long responseTimeoutMs;
+
+       JettyHttpTransport(JettyHttpTransportBuilder builder) throws Exception {
+               this.httpClient = builder.httpClient != null ? 
builder.httpClient : new HttpClient();
+               this.responseTimeoutMs = builder.responseTimeoutMs;
+               if (!this.httpClient.isStarted())
+                       this.httpClient.start();
+       }
+
+       /**
+        * Returns a new builder for this transport.
+        *
+        * @return A new builder. Never <jk>null</jk>.
+        */
+       public static JettyHttpTransportBuilder builder() {
+               return new JettyHttpTransportBuilder();
+       }
+
+       /**
+        * Returns a new instance backed by a default {@link HttpClient}.
+        *
+        * @return A new instance. Never <jk>null</jk>.
+        * @throws Exception If the Jetty {@link HttpClient} cannot be started.
+        */
+       public static JettyHttpTransport create() throws Exception {
+               return builder().build();
+       }
+
+       @Override /* HttpTransport */
+       public TransportResponse execute(TransportRequest request) throws 
TransportException {
+               var jettyRequest = buildJettyRequest(request);
+               var listener = new InputStreamResponseListener();
+               jettyRequest.send(listener);
+               Response jettyResponse;
+               try {
+                       jettyResponse = responseTimeoutMs > 0
+                               ? listener.get(responseTimeoutMs, 
TimeUnit.MILLISECONDS)
+                               : listener.get(Long.MAX_VALUE, 
TimeUnit.MILLISECONDS);
+               } catch (InterruptedException e) {
+                       Thread.currentThread().interrupt();
+                       throw new TransportException("HTTP request 
interrupted", e);
+               } catch (TimeoutException e) {
+                       throw new TransportException("HTTP request timed out", 
e);
+               } catch (ExecutionException e) {
+                       throw new TransportException("HTTP transport error: " + 
e.getCause().getMessage(), e.getCause());
+               }
+               return buildTransportResponse(jettyResponse, 
listener.getInputStream());
+       }
+
+       @Override /* Closeable */
+       public void close() throws IOException {
+               try {
+                       httpClient.stop();
+               } catch (Exception e) {
+                       throw new IOException("Failed to stop Jetty HttpClient: 
" + e.getMessage(), e);
+               }
+       }
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // Internal helpers
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       private Request buildJettyRequest(TransportRequest request) {
+               var jettyRequest = 
httpClient.newRequest(request.getUri()).method(request.getMethod());
+               for (var h : request.getHeaders())
+                       jettyRequest.headers(fields -> fields.add(h.name(), 
h.value()));
+               var body = request.getBody();
+               if (body != null)
+                       jettyRequest.body(buildRequestContent(body));
+               return jettyRequest;
+       }
+
+       private static OutputStreamRequestContent 
buildRequestContent(TransportBody body) {
+               var ct = body.getContentType();
+               var contentType = ct != null ? ct : "application/octet-stream";
+               var content = new OutputStreamRequestContent(contentType);
+               // Write body to the content's OutputStream on a background 
thread so Jetty can stream it concurrently.
+               var writer = new Thread(() -> {
+                       try (var out = content.getOutputStream()) {
+                               body.writeTo(out);
+                       } catch (IOException e) {
+                               // Closing the stream on error signals Jetty to 
abort the request.
+                       }
+               }, "juneau-ng-body-writer");
+               writer.setDaemon(true);
+               writer.start();
+               return content;
+       }
+
+       private static TransportResponse buildTransportResponse(Response 
jettyResponse, InputStream bodyStream) {
+               var builder = TransportResponse.builder()
+                       .statusCode(jettyResponse.getStatus())
+                       .reasonPhrase(jettyResponse.getReason())
+                       .body(bodyStream);
+               jettyResponse.getHeaders().forEach(field -> 
builder.header(field.getName(), field.getValue()));
+               return builder.build();
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportBuilder.java
 
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportBuilder.java
new file mode 100644
index 0000000000..18a2adbc7e
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportBuilder.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.rest.client.jetty;
+
+import org.eclipse.jetty.client.*;
+
+/**
+ * Fluent builder for {@link JettyHttpTransport}.
+ *
+ * <p>
+ * Obtain an instance via {@link JettyHttpTransport#builder()}.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the 
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in 
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client";>juneau-ng 
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class JettyHttpTransportBuilder {
+
+       HttpClient httpClient;
+       long responseTimeoutMs = 30_000;
+
+       JettyHttpTransportBuilder() {}
+
+       /**
+        * Sets the underlying {@link HttpClient} to use.
+        *
+        * <p>
+        * If not set, a default client is created via {@code new HttpClient()}.
+        *
+        * <p>
+        * If the provided client has not been started, {@link 
JettyHttpTransport} will start it automatically.
+        * When {@link JettyHttpTransport#close()} is called, the client will 
be stopped.
+        *
+        * @param value The client to use. Must not be <jk>null</jk>.
+        * @return This object.
+        */
+       public JettyHttpTransportBuilder httpClient(HttpClient value) {
+               httpClient = value;
+               return this;
+       }
+
+       /**
+        * Sets the maximum time to wait for the response headers to arrive 
after the request is sent.
+        *
+        * <p>
+        * This timeout covers the time from when the request is fully sent 
until the first response line and headers
+        * are received.  It does not limit how long the response body may take 
to stream.
+        * Defaults to {@code 30_000} ms (30 seconds).
+        *
+        * @param value The timeout in milliseconds. Use {@code 0} or a 
negative value to wait indefinitely.
+        * @return This object.
+        */
+       public JettyHttpTransportBuilder responseTimeoutMs(long value) {
+               responseTimeoutMs = value;
+               return this;
+       }
+
+       /**
+        * Builds and returns the {@link JettyHttpTransport}.
+        *
+        * @return A new instance. Never <jk>null</jk>.
+        * @throws Exception If the Jetty {@link HttpClient} cannot be started.
+        */
+       public JettyHttpTransport build() throws Exception {
+               return new JettyHttpTransport(this);
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportProvider.java
 
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportProvider.java
new file mode 100644
index 0000000000..75c85a9732
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/java/org/apache/juneau/ng/rest/client/jetty/JettyHttpTransportProvider.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.rest.client.jetty;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransportProvider} implementation that supplies a {@link 
JettyHttpTransport}.
+ *
+ * <p>
+ * Registered via {@code 
META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider} so 
that
+ * {@code NgRestClient} can auto-discover Jetty 12 when this module is on the 
classpath.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the 
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in 
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client";>juneau-ng 
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class JettyHttpTransportProvider implements HttpTransportProvider 
{
+
+       @Override /* HttpTransportProvider */
+       public int getPriority() {
+               return 75;
+       }
+
+       @Override /* HttpTransportProvider */
+       public boolean isAvailable() {
+               try {
+                       Class.forName("org.eclipse.jetty.client.HttpClient");
+                       return true;
+               } catch (ClassNotFoundException e) {
+                       return false;
+               }
+       }
+
+       @Override /* HttpTransportProvider */
+       public HttpTransport create() {
+               try {
+                       return JettyHttpTransport.create();
+               } catch (Exception e) {
+                       throw new RuntimeException("Failed to create 
JettyHttpTransport: " + e.getMessage(), e);
+               }
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-jetty/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
 
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
new file mode 100644
index 0000000000..4e96a7d483
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-jetty/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+org.apache.juneau.ng.rest.client.jetty.JettyHttpTransportProvider
diff --git a/juneau-rest/juneau-ng-rest-client-okhttp/pom.xml 
b/juneau-rest/juneau-ng-rest-client-okhttp/pom.xml
new file mode 100644
index 0000000000..45940dd6e5
--- /dev/null
+++ b/juneau-rest/juneau-ng-rest-client-okhttp/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+
+       <modelVersion>4.0.0</modelVersion>
+
+       <parent>
+               <groupId>org.apache.juneau</groupId>
+               <artifactId>juneau-rest</artifactId>
+               <version>9.2.1-SNAPSHOT</version>
+       </parent>
+
+       <artifactId>juneau-ng-rest-client-okhttp</artifactId>
+       <name>Apache Juneau NG REST Client — OkHttp Transport</name>
+       <description>OkHttp 5.x transport adapter for the next-generation 
Juneau REST client.</description>
+       <packaging>bundle</packaging>
+
+       <properties>
+               
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+       </properties>
+
+       <dependencies>
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-rest-client</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>com.squareup.okhttp3</groupId>
+                       <artifactId>okhttp-jvm</artifactId>
+                       <version>5.2.0</version>
+               </dependency>
+       </dependencies>
+
+       <build>
+               <plugins>
+                       <plugin>
+                               <groupId>org.apache.felix</groupId>
+                               <artifactId>maven-bundle-plugin</artifactId>
+                               <extensions>true</extensions>
+                               <configuration>
+                                       
<supportIncrementalBuild>true</supportIncrementalBuild>
+                               </configuration>
+                               <executions>
+                               <execution>
+                                       <id>bundle-manifest</id>
+                                       <phase>process-classes</phase>
+                                       <goals>
+                                               <goal>manifest</goal>
+                                       </goals>
+                               </execution>
+                       </executions>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-source-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <id>attach-sources</id>
+                                               <phase>verify</phase>
+                                               <goals>
+                                                       <goal>jar-no-fork</goal>
+                                               </goals>
+                                       </execution>
+                               </executions>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-jar-plugin</artifactId>
+                       </plugin>
+               </plugins>
+       </build>
+</project>
diff --git 
a/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransport.java
 
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransport.java
new file mode 100644
index 0000000000..e1750e0650
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransport.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.rest.client.okhttp;
+
+import java.io.*;
+
+import okhttp3.*;
+import okio.*;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransport} implementation backed by OkHttp 5.x.
+ *
+ * <p>
+ * This transport is auto-discovered via {@link java.util.ServiceLoader} when
+ * {@code com.squareup.okhttp3:okhttp-jvm} is on the classpath.  You can also 
instantiate it explicitly:
+ *
+ * <p class='bjava'>
+ *     <jv>transport</jv> = OkHttpTransport.<jsm>builder</jsm>()
+ *             .httpClient(<jk>new</jk> OkHttpClient())
+ *             .build();
+ *
+ *     <jv>client</jv> = NgRestClient.<jsm>builder</jsm>()
+ *             .transport(<jv>transport</jv>)
+ *             .build();
+ * </p>
+ *
+ * <p>
+ * OkHttp's {@link RequestBody#writeTo(BufferedSink)} streams the request body 
directly to the socket
+ * without in-memory buffering for bodies with a known content length.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the 
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in 
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client";>juneau-ng 
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class OkHttpTransport implements HttpTransport {
+
+       private final OkHttpClient httpClient;
+
+       OkHttpTransport(OkHttpTransportBuilder builder) {
+               this.httpClient = builder.httpClient != null ? 
builder.httpClient : new OkHttpClient();
+       }
+
+       /**
+        * Returns a new builder for this transport.
+        *
+        * @return A new builder. Never <jk>null</jk>.
+        */
+       public static OkHttpTransportBuilder builder() {
+               return new OkHttpTransportBuilder();
+       }
+
+       /**
+        * Returns a new instance backed by a default {@link OkHttpClient}.
+        *
+        * @return A new instance. Never <jk>null</jk>.
+        */
+       public static OkHttpTransport create() {
+               return builder().build();
+       }
+
+       @Override /* HttpTransport */
+       public TransportResponse execute(TransportRequest request) throws 
TransportException {
+               var okRequest = buildOkRequest(request);
+               Response okResponse;
+               try {
+                       okResponse = httpClient.newCall(okRequest).execute();
+               } catch (IOException e) {
+                       throw new TransportException("HTTP transport error: " + 
e.getMessage(), e);
+               }
+               return buildTransportResponse(okResponse);
+       }
+
+       @Override /* Closeable */
+       public void close() {
+               httpClient.dispatcher().executorService().shutdown();
+               httpClient.connectionPool().evictAll();
+       }
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // Internal helpers
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       private static Request buildOkRequest(TransportRequest request) throws 
TransportException {
+               var builder = new 
Request.Builder().url(request.getUri().toString());
+               for (var h : request.getHeaders())
+                       builder.addHeader(h.name(), h.value());
+               builder.method(request.getMethod(), 
buildOkBody(request.getBody(), request.getMethod()));
+               return builder.build();
+       }
+
+       private static RequestBody buildOkBody(TransportBody body, String 
method) {
+               if (body == null) {
+                       // OkHttp requires a non-null body for POST/PUT/PATCH 
even if empty
+                       return requiresBody(method) ? RequestBody.create(new 
byte[0], null) : null;
+               }
+               var ct = body.getContentType();
+               var mediaType = ct != null ? MediaType.parse(ct) : null;
+               return new RequestBody() {
+                       @Override
+                       public MediaType contentType() {
+                               return mediaType;
+                       }
+
+                       @Override
+                       public long contentLength() {
+                               return body.getContentLength();
+                       }
+
+                       @Override
+                       public void writeTo(BufferedSink sink) throws 
IOException {
+                               body.writeTo(sink.outputStream());
+                       }
+               };
+       }
+
+       private static boolean requiresBody(String method) {
+               return "POST".equalsIgnoreCase(method) || 
"PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method);
+       }
+
+       @SuppressWarnings({
+               "resource" // okResponse is closed via 
TransportResponse.closeCallback
+       })
+       private static TransportResponse buildTransportResponse(Response 
okResponse) throws TransportException {
+               var builder = TransportResponse.builder()
+                       .statusCode(okResponse.code())
+                       .reasonPhrase(okResponse.message())
+                       .closeCallback(okResponse);
+               for (var i = 0; i < okResponse.headers().size(); i++)
+                       builder.header(okResponse.headers().name(i), 
okResponse.headers().value(i));
+               var responseBody = okResponse.body();
+               if (responseBody != null) {
+                       try {
+                               builder.body(responseBody.byteStream());
+                       } catch (Exception e) {
+                               throw new TransportException("Failed to read 
response body: " + e.getMessage(), e);
+                       }
+               }
+               return builder.build();
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportBuilder.java
 
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportBuilder.java
new file mode 100644
index 0000000000..dcc79d27ae
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportBuilder.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.rest.client.okhttp;
+
+import okhttp3.*;
+
+/**
+ * Fluent builder for {@link OkHttpTransport}.
+ *
+ * <p>
+ * Obtain an instance via {@link OkHttpTransport#builder()}.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the 
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in 
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client";>juneau-ng 
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class OkHttpTransportBuilder {
+
+       OkHttpClient httpClient;
+
+       OkHttpTransportBuilder() {}
+
+       /**
+        * Sets the underlying {@link OkHttpClient} to use.
+        *
+        * <p>
+        * If not set, a default client is created via {@code new 
OkHttpClient()}.
+        *
+        * <p>
+        * OkHttp recommends sharing a single {@link OkHttpClient} instance 
across the application.
+        * When sharing, the caller is responsible for lifecycle management; 
{@link OkHttpTransport#close()}
+        * will still attempt to drain the client's connection pool and 
executor.
+        *
+        * @param value The client to use. Must not be <jk>null</jk>.
+        * @return This object.
+        */
+       public OkHttpTransportBuilder httpClient(OkHttpClient value) {
+               httpClient = value;
+               return this;
+       }
+
+       /**
+        * Builds and returns the {@link OkHttpTransport}.
+        *
+        * @return A new instance. Never <jk>null</jk>.
+        */
+       public OkHttpTransport build() {
+               return new OkHttpTransport(this);
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportProvider.java
 
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportProvider.java
new file mode 100644
index 0000000000..faaba748c1
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/java/org/apache/juneau/ng/rest/client/okhttp/OkHttpTransportProvider.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.rest.client.okhttp;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransportProvider} implementation that supplies an {@link 
OkHttpTransport}.
+ *
+ * <p>
+ * Registered via {@code 
META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider} so 
that
+ * {@code NgRestClient} can auto-discover OkHttp 5.x when this module is on 
the classpath.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the 
next-generation REST client and HTTP stack
+ * ({@code org.apache.juneau.ng.*}).
+ * It is not API-frozen: binary- and source-incompatible changes may appear in 
the <b>next major</b> Juneau release
+ * (and possibly earlier).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ *     <li class='link'><a class="doclink" 
href="https://juneau.apache.org/docs/topics/juneau-ng-rest-client";>juneau-ng 
REST client</a>
+ * </ul>
+ *
+ * @since 9.2.1
+ */
+public final class OkHttpTransportProvider implements HttpTransportProvider {
+
+       @Override /* HttpTransportProvider */
+       public int getPriority() {
+               return 70;
+       }
+
+       @Override /* HttpTransportProvider */
+       public boolean isAvailable() {
+               try {
+                       Class.forName("okhttp3.OkHttpClient");
+                       return true;
+               } catch (ClassNotFoundException e) {
+                       return false;
+               }
+       }
+
+       @Override /* HttpTransportProvider */
+       public HttpTransport create() {
+               return OkHttpTransport.create();
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-okhttp/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
 
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
new file mode 100644
index 0000000000..aba18514eb
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-okhttp/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+org.apache.juneau.ng.rest.client.okhttp.OkHttpTransportProvider
diff --git a/juneau-rest/pom.xml b/juneau-rest/pom.xml
index 896d9e23f5..1039fd7138 100644
--- a/juneau-rest/pom.xml
+++ b/juneau-rest/pom.xml
@@ -40,6 +40,8 @@
                <module>juneau-ng-rest-client-apache-httpclient-45</module>
                <module>juneau-ng-rest-client-apache-httpclient-50</module>
                <module>juneau-ng-rest-client-java-httpclient</module>
+               <module>juneau-ng-rest-client-okhttp</module>
+               <module>juneau-ng-rest-client-jetty</module>
        </modules>
 
        <build>
diff --git a/juneau-utest/pom.xml b/juneau-utest/pom.xml
index 3a9955ec6d..28e4f6594d 100644
--- a/juneau-utest/pom.xml
+++ b/juneau-utest/pom.xml
@@ -100,6 +100,16 @@
                        
<artifactId>juneau-ng-rest-client-java-httpclient</artifactId>
                        <version>${project.version}</version>
                </dependency>
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-ng-rest-client-okhttp</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-ng-rest-client-jetty</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
                <!-- Force commons-codec >= 1.13 to satisfy Jena's MurmurHash3 
requirement;
                     httpclient:4.5.14 pulls in 1.11 which pre-dates 
MurmurHash3. -->
                <dependency>
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JettyHttpTransport_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JettyHttpTransport_Test.java
new file mode 100644
index 0000000000..a752c9118e
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JettyHttpTransport_Test.java
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.*;
+
+import org.apache.juneau.ng.http.entity.*;
+import org.apache.juneau.ng.rest.client.*;
+import org.apache.juneau.ng.rest.client.jetty.*;
+import org.eclipse.jetty.client.*;
+import org.junit.jupiter.api.*;
+
+import com.sun.net.httpserver.*;
+
+/**
+ * Integration tests for {@link JettyHttpTransport} against a real embedded 
HTTP server.
+ */
+@SuppressWarnings("restriction")
+public class JettyHttpTransport_Test {
+
+       private static HttpServer server;
+       private static int port;
+
+       @BeforeAll
+       static void startServer() throws IOException {
+               server = HttpServer.create(new InetSocketAddress(0), 0);
+               port = server.getAddress().getPort();
+
+               server.createContext("/hello", exchange -> {
+                       var body = "Hello, 
World!".getBytes(StandardCharsets.UTF_8);
+                       exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
+                       exchange.sendResponseHeaders(200, body.length);
+                       exchange.getResponseBody().write(body);
+                       exchange.close();
+               });
+
+               server.createContext("/echo-method", exchange -> {
+                       var body = 
exchange.getRequestMethod().getBytes(StandardCharsets.UTF_8);
+                       exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
+                       exchange.sendResponseHeaders(200, body.length);
+                       exchange.getResponseBody().write(body);
+                       exchange.close();
+               });
+
+               server.createContext("/echo-body", exchange -> {
+                       var requestBody = 
exchange.getRequestBody().readAllBytes();
+                       exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
+                       exchange.sendResponseHeaders(200, requestBody.length);
+                       exchange.getResponseBody().write(requestBody);
+                       exchange.close();
+               });
+
+               server.createContext("/echo-header", exchange -> {
+                       var headerValue = 
exchange.getRequestHeaders().getFirst("X-Custom");
+                       var body = (headerValue != null ? headerValue : 
"missing").getBytes(StandardCharsets.UTF_8);
+                       exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
+                       exchange.sendResponseHeaders(200, body.length);
+                       exchange.getResponseBody().write(body);
+                       exchange.close();
+               });
+
+               server.createContext("/not-found", exchange -> {
+                       exchange.sendResponseHeaders(404, -1);
+                       exchange.close();
+               });
+
+               server.start();
+       }
+
+       @AfterAll
+       static void stopServer() {
+               if (server != null)
+                       server.stop(0);
+       }
+
+       private String rootUrl() {
+               return "http://localhost:"; + port;
+       }
+
+       // 
=================================================================================================================
+       // A — Basic connectivity
+       // 
=================================================================================================================
+
+       @Test
+       void a01_get_basicResponse() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/hello").run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("Hello, World!", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void a02_get_statusCode404() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/not-found").run()) {
+                               assertEquals(404, response.getStatusCode());
+                       }
+               }
+       }
+
+       @Test
+       void a03_get_responseHeader() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/hello").run()) {
+                               var ct = 
response.getFirstHeader("Content-Type");
+                               assertNotNull(ct);
+                               assertTrue(ct.value().startsWith("text/plain"), 
"Expected text/plain but got: " + ct.value());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // B — HTTP methods
+       // 
=================================================================================================================
+
+       @Test
+       void b01_post_echosMethod() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.post("/echo-method")
+                                       .body(StringBody.of("", "text/plain"))
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("POST", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void b02_put_echosMethod() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.put("/echo-method")
+                                       .body(StringBody.of("", "text/plain"))
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("PUT", response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void b03_delete_echosMethod() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = 
client.delete("/echo-method").run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("DELETE", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // C — Request body
+       // 
=================================================================================================================
+
+       @Test
+       void c01_post_stringBody() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.post("/echo-body")
+                                       .body(StringBody.of("hello body", 
"text/plain"))
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("hello body", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void c02_post_byteArrayBody() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       var bytes = "byte 
content".getBytes(StandardCharsets.UTF_8);
+                       try (var response = client.post("/echo-body")
+                                       .body(ByteArrayBody.of(bytes, 
"application/octet-stream"))
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("byte content", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // D — Request headers
+       // 
=================================================================================================================
+
+       @Test
+       void d01_header_sentToServer() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/echo-header")
+                                       .header("X-Custom", "my-value")
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("my-value", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void d02_missingHeader_returnsDefault() throws Exception {
+               var transport = JettyHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/echo-header").run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("missing", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // E — Builder: explicit HttpClient
+       // 
=================================================================================================================
+
+       @Test
+       void e01_builder_withExplicitHttpClient() throws Exception {
+               var transport = JettyHttpTransport.builder()
+                       .httpClient(new HttpClient())
+                       .build();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/hello").run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("Hello, World!", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // F — Provider
+       // 
=================================================================================================================
+
+       @Test
+       void f01_provider_isAvailable() {
+               var provider = new JettyHttpTransportProvider();
+               assertTrue(provider.isAvailable());
+       }
+
+       @Test
+       void f02_provider_priority() {
+               var provider = new JettyHttpTransportProvider();
+               assertEquals(75, provider.getPriority());
+       }
+
+       @Test
+       void f03_provider_create() throws Exception {
+               var provider = new JettyHttpTransportProvider();
+               try (var transport = provider.create()) {
+                       assertNotNull(transport);
+               }
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/OkHttpTransport_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/OkHttpTransport_Test.java
new file mode 100644
index 0000000000..0f96a22c71
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/OkHttpTransport_Test.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.ng.rest;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.*;
+
+import okhttp3.*;
+
+import org.apache.juneau.ng.http.entity.*;
+import org.apache.juneau.ng.rest.client.*;
+import org.apache.juneau.ng.rest.client.okhttp.*;
+import org.junit.jupiter.api.*;
+
+import com.sun.net.httpserver.*;
+
+/**
+ * Integration tests for {@link OkHttpTransport} against a real embedded HTTP 
server.
+ */
+@SuppressWarnings("restriction")
+public class OkHttpTransport_Test {
+
+       private static HttpServer server;
+       private static int port;
+
+       @BeforeAll
+       static void startServer() throws IOException {
+               server = HttpServer.create(new InetSocketAddress(0), 0);
+               port = server.getAddress().getPort();
+
+               server.createContext("/hello", exchange -> {
+                       var body = "Hello, 
World!".getBytes(StandardCharsets.UTF_8);
+                       exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
+                       exchange.sendResponseHeaders(200, body.length);
+                       exchange.getResponseBody().write(body);
+                       exchange.close();
+               });
+
+               server.createContext("/echo-method", exchange -> {
+                       var body = 
exchange.getRequestMethod().getBytes(StandardCharsets.UTF_8);
+                       exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
+                       exchange.sendResponseHeaders(200, body.length);
+                       exchange.getResponseBody().write(body);
+                       exchange.close();
+               });
+
+               server.createContext("/echo-body", exchange -> {
+                       var requestBody = 
exchange.getRequestBody().readAllBytes();
+                       exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
+                       exchange.sendResponseHeaders(200, requestBody.length);
+                       exchange.getResponseBody().write(requestBody);
+                       exchange.close();
+               });
+
+               server.createContext("/echo-header", exchange -> {
+                       var headerValue = 
exchange.getRequestHeaders().getFirst("X-Custom");
+                       var body = (headerValue != null ? headerValue : 
"missing").getBytes(StandardCharsets.UTF_8);
+                       exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
+                       exchange.sendResponseHeaders(200, body.length);
+                       exchange.getResponseBody().write(body);
+                       exchange.close();
+               });
+
+               server.createContext("/not-found", exchange -> {
+                       exchange.sendResponseHeaders(404, -1);
+                       exchange.close();
+               });
+
+               server.start();
+       }
+
+       @AfterAll
+       static void stopServer() {
+               if (server != null)
+                       server.stop(0);
+       }
+
+       private String rootUrl() {
+               return "http://localhost:"; + port;
+       }
+
+       // 
=================================================================================================================
+       // A — Basic connectivity
+       // 
=================================================================================================================
+
+       @Test
+       void a01_get_basicResponse() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/hello").run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("Hello, World!", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void a02_get_statusCode404() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/not-found").run()) {
+                               assertEquals(404, response.getStatusCode());
+                       }
+               }
+       }
+
+       @Test
+       void a03_get_responseHeader() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/hello").run()) {
+                               var ct = 
response.getFirstHeader("Content-Type");
+                               assertNotNull(ct);
+                               assertTrue(ct.value().startsWith("text/plain"), 
"Expected text/plain but got: " + ct.value());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // B — HTTP methods
+       // 
=================================================================================================================
+
+       @Test
+       void b01_post_echosMethod() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.post("/echo-method")
+                                       .body(StringBody.of("", "text/plain"))
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("POST", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void b02_put_echosMethod() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.put("/echo-method")
+                                       .body(StringBody.of("", "text/plain"))
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("PUT", response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void b03_delete_echosMethod() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = 
client.delete("/echo-method").run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("DELETE", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // C — Request body
+       // 
=================================================================================================================
+
+       @Test
+       void c01_post_stringBody() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.post("/echo-body")
+                                       .body(StringBody.of("hello body", 
"text/plain"))
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("hello body", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void c02_post_byteArrayBody() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       var bytes = "byte 
content".getBytes(StandardCharsets.UTF_8);
+                       try (var response = client.post("/echo-body")
+                                       .body(ByteArrayBody.of(bytes, 
"application/octet-stream"))
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("byte content", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // D — Request headers
+       // 
=================================================================================================================
+
+       @Test
+       void d01_header_sentToServer() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/echo-header")
+                                       .header("X-Custom", "my-value")
+                                       .run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("my-value", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       @Test
+       void d02_missingHeader_returnsDefault() throws Exception {
+               var transport = OkHttpTransport.create();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/echo-header").run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("missing", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // E — Builder: explicit OkHttpClient
+       // 
=================================================================================================================
+
+       @Test
+       void e01_builder_withExplicitHttpClient() throws Exception {
+               var transport = OkHttpTransport.builder()
+                       .httpClient(new OkHttpClient())
+                       .build();
+               try (var client = 
NgRestClient.builder().transport(transport).rootUrl(rootUrl()).build()) {
+                       try (var response = client.get("/hello").run()) {
+                               assertEquals(200, response.getStatusCode());
+                               assertEquals("Hello, World!", 
response.getBodyAsString());
+                       }
+               }
+       }
+
+       // 
=================================================================================================================
+       // F — Provider
+       // 
=================================================================================================================
+
+       @Test
+       void f01_provider_isAvailable() {
+               var provider = new OkHttpTransportProvider();
+               assertTrue(provider.isAvailable());
+       }
+
+       @Test
+       void f02_provider_priority() {
+               var provider = new OkHttpTransportProvider();
+               assertEquals(70, provider.getPriority());
+       }
+
+       @Test
+       void f03_provider_create() throws Exception {
+               var provider = new OkHttpTransportProvider();
+               try (var transport = provider.create()) {
+                       assertNotNull(transport);
+               }
+       }
+}

Reply via email to