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 d1e0e9a056 Next Generation RestClient
d1e0e9a056 is described below

commit d1e0e9a0563a52f27c2208faf8a6972f5b3553ca
Author: James Bognar <[email protected]>
AuthorDate: Thu Apr 9 15:36:17 2026 -0400

    Next Generation RestClient
---
 .../pom.xml                                        |  88 +++++++
 .../apachehttpclient50/ApacheHc5Transport.java     | 143 +++++++++++
 .../ApacheHc5TransportBuilder.java                 |  70 ++++++
 .../ApacheHc5TransportProvider.java                |  61 +++++
 ...che.juneau.ng.rest.client.HttpTransportProvider |  16 ++
 .../juneau-ng-rest-client-java-httpclient/pom.xml  |  83 +++++++
 .../client/javahttpclient/JavaHttpTransport.java   | 140 +++++++++++
 .../javahttpclient/JavaHttpTransportBuilder.java   |  67 +++++
 .../javahttpclient/JavaHttpTransportProvider.java  |  61 +++++
 ...che.juneau.ng.rest.client.HttpTransportProvider |  16 ++
 juneau-rest/pom.xml                                |   2 +
 juneau-utest/pom.xml                               |  10 +
 .../juneau/ng/rest/ApacheHc5Transport_Test.java    | 274 ++++++++++++++++++++
 .../juneau/ng/rest/JavaHttpTransport_Test.java     | 275 +++++++++++++++++++++
 todo/restclient2-transport-abstraction.md          |   2 +-
 15 files changed, 1307 insertions(+), 1 deletion(-)

diff --git a/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/pom.xml 
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/pom.xml
new file mode 100644
index 0000000000..b2097f6b40
--- /dev/null
+++ b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/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-apache-httpclient-50</artifactId>
+       <name>Apache Juneau NG REST Client — Apache HttpClient 5.x 
Transport</name>
+       <description>Apache HttpClient 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>org.apache.httpcomponents.client5</groupId>
+                       <artifactId>httpclient5</artifactId>
+                       <version>5.5.2</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-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5Transport.java
 
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5Transport.java
new file mode 100644
index 0000000000..95375a4167
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5Transport.java
@@ -0,0 +1,143 @@
+/*
+ * 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.apachehttpclient50;
+
+import java.io.*;
+
+import org.apache.hc.client5.http.impl.classic.*;
+import org.apache.hc.core5.http.*;
+import org.apache.hc.core5.http.io.entity.*;
+import org.apache.hc.core5.http.io.support.*;
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransport} implementation backed by Apache HttpClient 5.x.
+ *
+ * <p>
+ * This transport is auto-discovered via {@link java.util.ServiceLoader} when
+ * {@code org.apache.httpcomponents.client5:httpclient5} is on the classpath.  
You can also instantiate it explicitly:
+ *
+ * <p class='bjava'>
+ *     <jv>transport</jv> = ApacheHc5Transport.<jsm>builder</jsm>()
+ *             .httpClient(HttpClients.createDefault())
+ *             .build();
+ *
+ *     <jv>client</jv> = NgRestClient.<jsm>builder</jsm>()
+ *             .transport(<jv>transport</jv>)
+ *             .build();
+ * </p>
+ *
+ * <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
+ */
+@SuppressWarnings({
+       "resource" // httpClient is owned by this transport and closed in 
close()
+})
+public final class ApacheHc5Transport implements HttpTransport {
+
+       private final CloseableHttpClient httpClient;
+
+       ApacheHc5Transport(ApacheHc5TransportBuilder builder) {
+               this.httpClient = builder.httpClient != null ? 
builder.httpClient : HttpClients.createDefault();
+       }
+
+       /**
+        * Returns a new builder for this transport.
+        *
+        * @return A new builder. Never <jk>null</jk>.
+        */
+       public static ApacheHc5TransportBuilder builder() {
+               return new ApacheHc5TransportBuilder();
+       }
+
+       /**
+        * Returns a new instance backed by a default {@link 
CloseableHttpClient}.
+        *
+        * @return A new instance. Never <jk>null</jk>.
+        */
+       public static ApacheHc5Transport create() {
+               return builder().build();
+       }
+
+       @Override /* HttpTransport */
+       public TransportResponse execute(TransportRequest request) throws 
TransportException {
+               var hcRequest = buildHcRequest(request);
+               CloseableHttpResponse hcResponse;
+               try {
+                       hcResponse = httpClient.execute(hcRequest);
+               } catch (IOException e) {
+                       throw new TransportException("HTTP transport error: " + 
e.getMessage(), e);
+               }
+               return buildTransportResponse(hcResponse);
+       }
+
+       @Override /* Closeable */
+       public void close() throws IOException {
+               httpClient.close();
+       }
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // Internal helpers
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       private static ClassicHttpRequest buildHcRequest(TransportRequest 
request) throws TransportException {
+               var builder = 
ClassicRequestBuilder.create(request.getMethod()).setUri(request.getUri());
+               for (var h : request.getHeaders())
+                       builder.addHeader(h.name(), h.value());
+               var body = request.getBody();
+               if (body != null)
+                       builder.setEntity(buildEntity(body));
+               try {
+                       return builder.build();
+               } catch (Exception e) {
+                       throw new TransportException("Failed to build HTTP 
request: " + e.getMessage(), e);
+               }
+       }
+
+       private static HttpEntity buildEntity(TransportBody body) {
+               var ct = body.getContentType();
+               var contentType = ct != null ? ContentType.parse(ct) : 
ContentType.APPLICATION_OCTET_STREAM;
+               return new EntityTemplate(body.getContentLength(), contentType, 
null, body::writeTo);
+       }
+
+       private static TransportResponse 
buildTransportResponse(CloseableHttpResponse hcResponse) throws 
TransportException {
+               var builder = TransportResponse.builder()
+                       .statusCode(hcResponse.getCode())
+                       .reasonPhrase(hcResponse.getReasonPhrase())
+                       .closeCallback(hcResponse);
+               for (var h : hcResponse.getHeaders())
+                       builder.header(h.getName(), h.getValue());
+               var entity = hcResponse.getEntity();
+               if (entity != null) {
+                       try {
+                               builder.body(entity.getContent());
+                       } catch (IOException e) {
+                               throw new TransportException("Failed to read 
response body: " + e.getMessage(), e);
+                       }
+               }
+               return builder.build();
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportBuilder.java
 
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportBuilder.java
new file mode 100644
index 0000000000..a4697c6146
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportBuilder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.apachehttpclient50;
+
+import org.apache.hc.client5.http.impl.classic.*;
+
+/**
+ * Fluent builder for {@link ApacheHc5Transport}.
+ *
+ * <p>
+ * Obtain an instance via {@link ApacheHc5Transport#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 ApacheHc5TransportBuilder {
+
+       CloseableHttpClient httpClient;
+
+       ApacheHc5TransportBuilder() {}
+
+       /**
+        * Sets the underlying {@link CloseableHttpClient} to use.
+        *
+        * <p>
+        * If not set, a default client is created via {@link 
HttpClients#createDefault()}.
+        *
+        * <p>
+        * The transport takes ownership of the client and will close it when 
{@link ApacheHc5Transport#close()} is called.
+        *
+        * @param value The client to use. Must not be <jk>null</jk>.
+        * @return This object.
+        */
+       public ApacheHc5TransportBuilder httpClient(CloseableHttpClient value) {
+               httpClient = value;
+               return this;
+       }
+
+       /**
+        * Builds and returns the {@link ApacheHc5Transport}.
+        *
+        * @return A new instance. Never <jk>null</jk>.
+        */
+       public ApacheHc5Transport build() {
+               return new ApacheHc5Transport(this);
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportProvider.java
 
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportProvider.java
new file mode 100644
index 0000000000..bed1d6ffe0
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/java/org/apache/juneau/ng/rest/client/apachehttpclient50/ApacheHc5TransportProvider.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.apachehttpclient50;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransportProvider} implementation that supplies an {@link 
ApacheHc5Transport}.
+ *
+ * <p>
+ * Registered via {@code 
META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider} so 
that
+ * {@code NgRestClient} can auto-discover Apache HttpClient 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 ApacheHc5TransportProvider implements HttpTransportProvider 
{
+
+       @Override /* HttpTransportProvider */
+       public int getPriority() {
+               return 60;
+       }
+
+       @Override /* HttpTransportProvider */
+       public boolean isAvailable() {
+               try {
+                       
Class.forName("org.apache.hc.client5.http.impl.classic.CloseableHttpClient");
+                       return true;
+               } catch (ClassNotFoundException e) {
+                       return false;
+               }
+       }
+
+       @Override /* HttpTransportProvider */
+       public HttpTransport create() {
+               return ApacheHc5Transport.create();
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
 
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
new file mode 100644
index 0000000000..9ed6dec432
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-apache-httpclient-50/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.apachehttpclient50.ApacheHc5TransportProvider
diff --git a/juneau-rest/juneau-ng-rest-client-java-httpclient/pom.xml 
b/juneau-rest/juneau-ng-rest-client-java-httpclient/pom.xml
new file mode 100644
index 0000000000..c2d222e942
--- /dev/null
+++ b/juneau-rest/juneau-ng-rest-client-java-httpclient/pom.xml
@@ -0,0 +1,83 @@
+<?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-java-httpclient</artifactId>
+       <name>Apache Juneau NG REST Client — JDK HttpClient Transport</name>
+       <description>JDK java.net.http.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>
+       </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-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransport.java
 
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransport.java
new file mode 100644
index 0000000000..b2fbdbc14b
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransport.java
@@ -0,0 +1,140 @@
+/*
+ * 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.javahttpclient;
+
+import java.io.*;
+import java.net.http.*;
+import java.net.http.HttpRequest.*;
+import java.net.http.HttpResponse.*;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransport} implementation backed by the JDK's built-in {@link 
java.net.http.HttpClient}.
+ *
+ * <p>
+ * No external dependencies are required — this transport is always available 
on Java 11+.
+ * It is auto-discovered via {@link java.util.ServiceLoader} and can also be 
instantiated explicitly:
+ *
+ * <p class='bjava'>
+ *     <jv>transport</jv> = JavaHttpTransport.<jsm>builder</jsm>()
+ *             .httpClient(HttpClient.newHttpClient())
+ *             .build();
+ *
+ *     <jv>client</jv> = NgRestClient.<jsm>builder</jsm>()
+ *             .transport(<jv>transport</jv>)
+ *             .build();
+ * </p>
+ *
+ * <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 JavaHttpTransport implements HttpTransport {
+
+       private final java.net.http.HttpClient httpClient;
+
+       JavaHttpTransport(JavaHttpTransportBuilder builder) {
+               this.httpClient = builder.httpClient != null ? 
builder.httpClient : java.net.http.HttpClient.newHttpClient();
+       }
+
+       /**
+        * Returns a new builder for this transport.
+        *
+        * @return A new builder. Never <jk>null</jk>.
+        */
+       public static JavaHttpTransportBuilder builder() {
+               return new JavaHttpTransportBuilder();
+       }
+
+       /**
+        * Returns a new instance backed by a default {@link 
java.net.http.HttpClient}.
+        *
+        * @return A new instance. Never <jk>null</jk>.
+        */
+       public static JavaHttpTransport create() {
+               return builder().build();
+       }
+
+       @Override /* HttpTransport */
+       public TransportResponse execute(TransportRequest request) throws 
TransportException {
+               HttpRequest jdkRequest;
+               try {
+                       jdkRequest = buildJdkRequest(request);
+               } catch (IOException e) {
+                       throw new TransportException("Failed to build HTTP 
request: " + e.getMessage(), e);
+               }
+               HttpResponse<InputStream> jdkResponse;
+               try {
+                       jdkResponse = httpClient.send(jdkRequest, 
BodyHandlers.ofInputStream());
+               } catch (IOException e) {
+                       throw new TransportException("HTTP transport error: " + 
e.getMessage(), e);
+               } catch (InterruptedException e) {
+                       Thread.currentThread().interrupt();
+                       throw new TransportException("HTTP request 
interrupted", e);
+               }
+               return buildTransportResponse(jdkResponse);
+       }
+
+       // 
-----------------------------------------------------------------------------------------------------------------
+       // Internal helpers
+       // 
-----------------------------------------------------------------------------------------------------------------
+
+       private static HttpRequest buildJdkRequest(TransportRequest request) 
throws IOException {
+               var builder = 
HttpRequest.newBuilder().uri(request.getUri()).method(request.getMethod(), 
buildBodyPublisher(request.getBody()));
+               for (var h : request.getHeaders())
+                       builder.header(h.name(), h.value());
+               return builder.build();
+       }
+
+       private static BodyPublisher buildBodyPublisher(TransportBody body) 
throws IOException {
+               if (body == null)
+                       return BodyPublishers.noBody();
+               // Use a pipe so body.writeTo() streams directly to the JDK 
client without full in-memory buffering.
+               // The writer runs on a daemon thread; the JDK client reads 
from the PipedInputStream on its own threads.
+               var in = new PipedInputStream();
+               var out = new PipedOutputStream(in);
+               var writer = new Thread(() -> {
+                       try (out) {
+                               body.writeTo(out);
+                       } catch (IOException e) {
+                               // Closing the pipe on error causes the reader 
to see an IOException,
+                               // which propagates to the JDK client as a send 
failure.
+                       }
+               }, "juneau-ng-body-writer");
+               writer.setDaemon(true);
+               writer.start();
+               return BodyPublishers.ofInputStream(() -> in);
+       }
+
+       private static TransportResponse 
buildTransportResponse(HttpResponse<InputStream> jdkResponse) {
+               var builder = TransportResponse.builder()
+                       .statusCode(jdkResponse.statusCode())
+                       .body(jdkResponse.body());
+               jdkResponse.headers().map().forEach((name, values) ->
+                       values.forEach(value -> builder.header(name, value)));
+               return builder.build();
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportBuilder.java
 
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportBuilder.java
new file mode 100644
index 0000000000..b3edc17f25
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportBuilder.java
@@ -0,0 +1,67 @@
+/*
+ * 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.javahttpclient;
+
+import java.net.http.*;
+
+/**
+ * Fluent builder for {@link JavaHttpTransport}.
+ *
+ * <p>
+ * Obtain an instance via {@link JavaHttpTransport#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 JavaHttpTransportBuilder {
+
+       HttpClient httpClient;
+
+       JavaHttpTransportBuilder() {}
+
+       /**
+        * Sets the underlying {@link HttpClient} to use.
+        *
+        * <p>
+        * If not set, a default client is created via {@link 
HttpClient#newHttpClient()}.
+        *
+        * @param value The client to use. Must not be <jk>null</jk>.
+        * @return This object.
+        */
+       public JavaHttpTransportBuilder httpClient(HttpClient value) {
+               httpClient = value;
+               return this;
+       }
+
+       /**
+        * Builds and returns the {@link JavaHttpTransport}.
+        *
+        * @return A new instance. Never <jk>null</jk>.
+        */
+       public JavaHttpTransport build() {
+               return new JavaHttpTransport(this);
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportProvider.java
 
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportProvider.java
new file mode 100644
index 0000000000..af7cb421e4
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/java/org/apache/juneau/ng/rest/client/javahttpclient/JavaHttpTransportProvider.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.javahttpclient;
+
+import org.apache.juneau.ng.rest.client.*;
+
+/**
+ * {@link HttpTransportProvider} implementation that supplies a {@link 
JavaHttpTransport}.
+ *
+ * <p>
+ * Registered via {@code 
META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider} so 
that
+ * {@code NgRestClient} can auto-discover the JDK HTTP client when no 
higher-priority transport is present.
+ *
+ * <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 JavaHttpTransportProvider implements HttpTransportProvider {
+
+       @Override /* HttpTransportProvider */
+       public int getPriority() {
+               return 80;
+       }
+
+       @Override /* HttpTransportProvider */
+       public boolean isAvailable() {
+               try {
+                       Class.forName("java.net.http.HttpClient");
+                       return true;
+               } catch (ClassNotFoundException e) {
+                       return false;
+               }
+       }
+
+       @Override /* HttpTransportProvider */
+       public HttpTransport create() {
+               return JavaHttpTransport.create();
+       }
+}
diff --git 
a/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
 
b/juneau-rest/juneau-ng-rest-client-java-httpclient/src/main/resources/META-INF/services/org.apache.juneau.ng.rest.client.HttpTransportProvider
new file mode 100644
index 0000000000..df29856c35
--- /dev/null
+++ 
b/juneau-rest/juneau-ng-rest-client-java-httpclient/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.javahttpclient.JavaHttpTransportProvider
diff --git a/juneau-rest/pom.xml b/juneau-rest/pom.xml
index cde05c0514..896d9e23f5 100644
--- a/juneau-rest/pom.xml
+++ b/juneau-rest/pom.xml
@@ -38,6 +38,8 @@
                <module>juneau-rest-client</module>
                <module>juneau-rest-mock</module>
                <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>
        </modules>
 
        <build>
diff --git a/juneau-utest/pom.xml b/juneau-utest/pom.xml
index 4d8794c9d0..3a9955ec6d 100644
--- a/juneau-utest/pom.xml
+++ b/juneau-utest/pom.xml
@@ -90,6 +90,16 @@
                        
<artifactId>juneau-ng-rest-client-apache-httpclient-45</artifactId>
                        <version>${project.version}</version>
                </dependency>
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       
<artifactId>juneau-ng-rest-client-apache-httpclient-50</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       
<artifactId>juneau-ng-rest-client-java-httpclient</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/ApacheHc5Transport_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/ApacheHc5Transport_Test.java
new file mode 100644
index 0000000000..ae4b8989d0
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/ApacheHc5Transport_Test.java
@@ -0,0 +1,274 @@
+/*
+ * 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.apachehttpclient50.*;
+import org.junit.jupiter.api.*;
+
+import com.sun.net.httpserver.*;
+
+/**
+ * Integration tests for {@link ApacheHc5Transport} against a real embedded 
HTTP server.
+ */
+@SuppressWarnings("restriction")
+public class ApacheHc5Transport_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 = ApacheHc5Transport.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 = ApacheHc5Transport.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 = ApacheHc5Transport.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 = ApacheHc5Transport.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 = ApacheHc5Transport.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 = ApacheHc5Transport.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 = ApacheHc5Transport.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 = ApacheHc5Transport.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 = ApacheHc5Transport.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 = ApacheHc5Transport.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 = ApacheHc5Transport.builder()
+                       
.httpClient(org.apache.hc.client5.http.impl.classic.HttpClients.createDefault())
+                       .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 ApacheHc5TransportProvider();
+               assertTrue(provider.isAvailable());
+       }
+
+       @Test
+       void f02_provider_priority() {
+               var provider = new ApacheHc5TransportProvider();
+               assertEquals(60, provider.getPriority());
+       }
+
+       @Test
+       void f03_provider_create() throws Exception {
+               var provider = new ApacheHc5TransportProvider();
+               try (var transport = provider.create()) {
+                       assertNotNull(transport);
+               }
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JavaHttpTransport_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JavaHttpTransport_Test.java
new file mode 100644
index 0000000000..0bebbe7bcf
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/JavaHttpTransport_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.net.http.*;
+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.javahttpclient.*;
+import org.junit.jupiter.api.*;
+
+import com.sun.net.httpserver.*;
+
+/**
+ * Integration tests for {@link JavaHttpTransport} against a real embedded 
HTTP server.
+ */
+@SuppressWarnings("restriction")
+public class JavaHttpTransport_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 = JavaHttpTransport.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 = JavaHttpTransport.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 = JavaHttpTransport.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 = JavaHttpTransport.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 = JavaHttpTransport.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 = JavaHttpTransport.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 = JavaHttpTransport.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 = JavaHttpTransport.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 = JavaHttpTransport.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 = JavaHttpTransport.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 = JavaHttpTransport.builder()
+                       .httpClient(HttpClient.newHttpClient())
+                       .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 JavaHttpTransportProvider();
+               assertTrue(provider.isAvailable());
+       }
+
+       @Test
+       void f02_provider_priority() {
+               var provider = new JavaHttpTransportProvider();
+               assertEquals(80, provider.getPriority());
+       }
+
+       @Test
+       void f03_provider_create() throws Exception {
+               var provider = new JavaHttpTransportProvider();
+               try (var transport = provider.create()) {
+                       assertNotNull(transport);
+               }
+       }
+}
diff --git a/todo/restclient2-transport-abstraction.md 
b/todo/restclient2-transport-abstraction.md
index a661fe575e..7ebef85dc7 100644
--- a/todo/restclient2-transport-abstraction.md
+++ b/todo/restclient2-transport-abstraction.md
@@ -107,7 +107,7 @@ New code is added as new packages inside **existing** 
modules where possible. On
 - **New package**: `org.apache.juneau.ng.http` — no `httpcore` or `httpclient` 
dependency in this package
 - **Contains**: JDK-native replacements for all ~186 types in 
`juneau-rest-common` that currently implement Apache HttpCore interfaces
 
-See [ng.http Package Design](#nghttp-package-design-in-juneau-rest-common) 
below.
+See ng.http Package Design below.
 
 ### 1. `juneau-rest-client` (Existing — new package added)
 

Reply via email to