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 b4a94c8152 test: TODO-11 NG REST client coverage closeout + 
cross-transport remote tests
b4a94c8152 is described below

commit b4a94c81523ab18fa83bb965e4be13cc63234672
Author: James Bognar <[email protected]>
AuthorDate: Fri May 15 12:16:20 2026 -0400

    test: TODO-11 NG REST client coverage closeout + cross-transport remote 
tests
---
 .../microservice/MicroserviceTestFixture.java      | 187 ++++++++++++
 .../org/apache/juneau/ng/NgPackageScanner.java     |  93 ++++++
 .../juneau/ng/http/HttpFactoryFacades_Test.java    | 125 ++++++++
 .../juneau/ng/http/entity/HttpBodies_Test.java     | 211 +++++++++++++
 .../juneau/ng/http/header/NgNamedHeaders_Test.java | 302 ++++++++++++++++++
 .../ng/http/header/PolymorphicHeaders_Test.java    | 177 +++++++++++
 .../apache/juneau/ng/http/part/HttpParts_Test.java |  91 ++++++
 .../ng/http/response/HttpStatusLineBean_Test.java  |  76 +++++
 .../ng/http/response/NgNamedResponses_Test.java    | 188 ++++++++++++
 .../ng/rest/NgRemoteInterfaceTransport_Test.java   | 340 +++++++++++++++++++++
 ...FINISHED-11b-restclient-ng-coverage-closeout.md |  64 ++++
 todo/TODO-11-restclient-ng-coverage-closeout.md    |  70 -----
 todo/TODO.md                                       |   2 -
 13 files changed, 1854 insertions(+), 72 deletions(-)

diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/microservice/MicroserviceTestFixture.java
 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/MicroserviceTestFixture.java
new file mode 100644
index 0000000000..931d4a6694
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/microservice/MicroserviceTestFixture.java
@@ -0,0 +1,187 @@
+/*
+ * 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.microservice;
+
+import java.net.*;
+import java.util.*;
+
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.eclipse.jetty.ee11.servlet.*;
+import org.eclipse.jetty.server.*;
+import org.junit.jupiter.api.extension.*;
+
+/**
+ * JUnit 5 extension that boots a {@link JettyMicroservice} on an ephemeral 
port for the duration of a test class.
+ *
+ * <p>
+ * Typical usage:
+ *
+ * <p class='bjava'>
+ *     <ja>@RegisterExtension</ja>
+ *     <jk>static</jk> MicroserviceTestFixture <jv>fixture</jv> = 
MicroserviceTestFixture.<jsm>create</jsm>()
+ *             .configurations(MyServerConfig.<jk>class</jk>);
+ *
+ *     <ja>@Configuration</ja>
+ *     <jk>static class</jk> MyServerConfig {{
+ *             <ja>@Bean</ja> Servlet myService() {{ <jk>return new</jk> 
MyRestService(); }}
+ *     }}
+ *
+ *     <ja>@Test</ja>
+ *     <jk>void</jk> exampleTest() {{
+ *             <jk>var</jk> rootUrl = <jv>fixture</jv>.getRootUrl();
+ *             <jc>// build a client against rootUrl ...</jc>
+ *     }}
+ * </p>
+ *
+ * <h5 class='section'>How it works:</h5>
+ * <ul>
+ *     <li>The fixture supplies its own {@link Server} {@link Bean} factory 
({@link EphemeralJettyServerConfig}) that
+ *             binds to port {@code 0} (OS-assigned ephemeral port). 
User-supplied configurations should typically
+ *             contribute {@code @Bean Servlet} definitions (which the {@link 
JettyMicroservice} auto-mounts at
+ *             {@code @Rest(path=...)}); a user-supplied {@code @Bean Server} 
will conflict and is not supported here.
+ *     <li>{@link #beforeAll(ExtensionContext) beforeAll} builds the 
microservice, calls
+ *             {@link JettyMicroservice#createServer() createServer()}, and 
then
+ *             {@link JettyMicroservice#start() start()}.
+ *     <li>{@link #afterAll(ExtensionContext) afterAll} calls {@link 
JettyMicroservice#stop() stop()} so the bound
+ *             port is released and {@code @PreDestroy} hooks on bean-store 
beans fire.
+ * </ul>
+ *
+ * <p>
+ * The fixture is intended to amortize Jetty-startup cost across a whole test 
class — combined with the dynamic
+ * {@link org.apache.juneau.rest.RestChildren#addChild RestChildren.addChild} 
/ {@code removeChild} API, individual
+ * test methods can mount and unmount their own child resources against a 
single long-running server.
+ */
+public final class MicroserviceTestFixture implements BeforeAllCallback, 
AfterAllCallback {
+
+       private final List<Class<?>> configurations = new ArrayList<>();
+       private JettyMicroservice microservice;
+       private URI rootUrl;
+
+       private MicroserviceTestFixture() {}
+
+       /**
+        * Creates a new fixture.
+        *
+        * @return A new fixture instance.
+        */
+       public static MicroserviceTestFixture create() {
+               return new MicroserviceTestFixture();
+       }
+
+       /**
+        * Registers one or more {@code @Configuration} classes whose {@code 
@Bean Servlet} methods will be auto-mounted
+        * by the microservice. May be called multiple times to append.
+        *
+        * @param cs The configuration classes.
+        * @return This fixture (fluent).
+        */
+       public MicroserviceTestFixture configurations(Class<?>... cs) {
+               Collections.addAll(configurations, cs);
+               return this;
+       }
+
+       /**
+        * Returns the root URI of the running microservice (after {@link 
#beforeAll(ExtensionContext)} has fired).
+        *
+        * @return The root URI, e.g. {@code http://localhost:54321/}.
+        */
+       public URI getRootUrl() {
+               return rootUrl;
+       }
+
+       /**
+        * Returns the actual port the server is listening on (post-{@code 
start()}).
+        *
+        * @return The bound port.
+        */
+       public int getPort() {
+               return rootUrl.getPort();
+       }
+
+       /**
+        * Returns the underlying microservice instance, useful for 
fixture-scoped child-resource mutations.
+        *
+        * @return The microservice.
+        */
+       public JettyMicroservice getMicroservice() {
+               return microservice;
+       }
+
+       @Override
+       public void beforeAll(ExtensionContext ctx) throws Exception {
+               // The user's configurations come first (so @Bean Servlet 
methods are visible). Our default Server
+               // factory is registered last via configurations() — 
JettyMicroservice resolves Server from the bean
+               // store by type, and BeanStore.getBean(...) returns the first 
registered match.
+               var classes = new ArrayList<>(configurations);
+               classes.add(EphemeralJettyServerConfig.class);
+               microservice = JettyMicroservice.create()
+                       .configurations(classes.toArray(new Class<?>[0]))
+                       .build();
+               microservice.start();
+               // JettyMicroservice.getURI() uses 
InetAddress.getLocalHost().getHostName() (may resolve to a non-loopback
+               // IP on some machines) and ServerConnector.getPort() (returns 
the configured port = 0, not the bound port).
+               // For test fixtures we want a deterministic loopback URL with 
the actual bound port.
+               var localPort = -1;
+               for (var c : microservice.getServer().getConnectors()) {
+                       if (c instanceof ServerConnector sc) {
+                               localPort = sc.getLocalPort();
+                               break;
+                       }
+               }
+               if (localPort <= 0)
+                       throw new IllegalStateException("Could not determine 
local port of ServerConnector after start.");
+               rootUrl = URI.create("http://localhost:"; + localPort);
+       }
+
+       @Override
+       public void afterAll(ExtensionContext ctx) throws Exception {
+               if (microservice != null)
+                       microservice.stop();
+       }
+
+       /**
+        * Default {@code @Configuration} that supplies a Jetty {@link Server} 
bound to port {@code 0}.
+        *
+        * <p>
+        * The server has a single {@link ServerConnector} on port 0 
(OS-assigned) and a single root
+        * {@link ServletContextHandler} at context path {@code "/"}. {@code 
JettyMicroservice} discovers the handler
+        * via the {@code "ServletContextHandler"} server attribute (the same 
convention used by {@code jetty.xml}).
+        */
+       @Configuration
+       public static class EphemeralJettyServerConfig {
+
+               /**
+                * Provides the bean-supplied Jetty {@link Server} that the 
microservice consumes during
+                * {@link JettyMicroservice#createServer()}.
+                *
+                * @return A configured {@link Server} bound to port 0.
+                */
+               @Bean
+               public Server jettyServer() {
+                       var server = new Server();
+                       var connector = new ServerConnector(server);
+                       connector.setPort(0);
+                       server.addConnector(connector);
+                       var sch = new ServletContextHandler();
+                       sch.setContextPath("/");
+                       server.setAttribute("ServletContextHandler", sch);
+                       server.setHandler(sch);
+                       return server;
+               }
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/NgPackageScanner.java 
b/juneau-utest/src/test/java/org/apache/juneau/ng/NgPackageScanner.java
new file mode 100644
index 0000000000..3189b10ff1
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/ng/NgPackageScanner.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+import java.io.*;
+import java.net.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.jar.*;
+
+/**
+ * Test-only helper for enumerating concrete Java classes in a package on the 
classpath.
+ *
+ * <p>
+ * Used by the parametric {@code Ng*_Test} classes to walk every public class 
in
+ * {@code org.apache.juneau.ng.http.response} / {@code .header} without having 
to hard-code
+ * the (large and frequently-changing) list.
+ */
+public final class NgPackageScanner {
+
+       private NgPackageScanner() {}
+
+       /**
+        * Returns every concrete class in {@code packageName}, located via 
{@code sentinel}'s defining JAR/dir.
+        *
+        * <p>
+        * Scans the JAR or directory that contains {@code sentinel}, listing 
{@code .class} entries under the
+        * package directory and loading each via the system classloader. 
Excludes interfaces, abstract classes,
+        * inner classes, and anonymous classes.
+        *
+        * @param packageName Dotted package name to scan (no trailing slash). 
Must not be {@code null}.
+        * @param sentinel    A class that lives in the JAR/dir to scan (any 
class from the target module works).
+        *                    Must not be {@code null}.
+        * @return Sorted-by-name list of concrete classes found in the package.
+        * @throws Exception if the JAR/dir cannot be opened.
+        */
+       public static List<Class<?>> enumerateConcreteClasses(String 
packageName, Class<?> sentinel) throws Exception {
+               var location = 
sentinel.getProtectionDomain().getCodeSource().getLocation();
+               var dirPath = packageName.replace('.', '/');
+               var out = new ArrayList<Class<?>>();
+               if (location.toString().endsWith(".jar")) {
+                       try (var jar = new JarFile(new File(location.toURI()))) 
{
+                               for (var e : (Iterable<JarEntry>) () -> 
jar.entries().asIterator()) {
+                                       var name = e.getName();
+                                       if (name.startsWith(dirPath + "/") && 
name.endsWith(".class") && ! name.contains("$")) {
+                                               var rel = 
name.substring(dirPath.length() + 1, name.length() - ".class".length());
+                                               if (rel.contains("/"))
+                                                       continue; // skip 
sub-packages
+                                               maybeAdd(out, packageName + "." 
+ rel);
+                                       }
+                               }
+                       }
+               } else {
+                       var dir = Path.of(URI.create(location.toString() + 
dirPath));
+                       if (! Files.isDirectory(dir))
+                               return out;
+                       try (var stream = Files.list(dir)) {
+                               stream.forEach(p -> {
+                                       var f = p.getFileName().toString();
+                                       if (f.endsWith(".class") && ! 
f.contains("$"))
+                                               maybeAdd(out, packageName + "." 
+ f.substring(0, f.length() - ".class".length()));
+                               });
+                       }
+               }
+               out.sort(Comparator.comparing(Class::getSimpleName));
+               return out;
+       }
+
+       private static void maybeAdd(List<Class<?>> out, String fqn) {
+               try {
+                       var c = Class.forName(fqn, false, 
NgPackageScanner.class.getClassLoader());
+                       if (c.isInterface() || 
java.lang.reflect.Modifier.isAbstract(c.getModifiers()))
+                               return;
+                       out.add(c);
+               } catch (ClassNotFoundException ignored) {
+                       // Skip — not loadable from the test classpath.
+               }
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
new file mode 100644
index 0000000000..0b68495f96
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/HttpFactoryFacades_Test.java
@@ -0,0 +1,125 @@
+/*
+ * 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.http;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.net.*;
+import java.time.*;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.http.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.ng.http.part.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Parametric coverage for the static-factory facade classes ({@link 
HttpHeaders}, {@link HttpBodies},
+ * {@link HttpResponses}).
+ *
+ * <p>
+ * Each facade is a thin DSL over the underlying named-type factories — 
invoking every public static
+ * method with a type-appropriate sample is the cheapest way to close out 
coverage on a couple of
+ * hundred otherwise-uncovered delegation lines.
+ */
+class HttpFactoryFacades_Test extends TestBase {
+
+       static Stream<Method> facadeMethods() {
+               return Stream.of(HttpHeaders.class, HttpBodies.class, 
HttpResponses.class)
+                       .flatMap(c -> Arrays.stream(c.getDeclaredMethods()))
+                       .filter(m -> Modifier.isPublic(m.getModifiers()))
+                       .filter(m -> Modifier.isStatic(m.getModifiers()));
+       }
+
+       @ParameterizedTest(name = "{0}")
+       @MethodSource("facadeMethods")
+       void invokeWithSampleArgs(Method m) throws Exception {
+               var args = sampleArgs(m);
+               if (args == null)
+                       return; // unsupported parameter combination — covered 
elsewhere
+               var result = m.invoke(null, args);
+               if (m.getReturnType() != void.class)
+                       assertNotNull(result, 
m.getDeclaringClass().getSimpleName() + "." + m.getName());
+       }
+
+       private static Object[] sampleArgs(Method m) {
+               var params = m.getParameterTypes();
+               var out = new Object[params.length];
+               for (var i = 0; i < params.length; i++) {
+                       var v = sampleFor(params[i], m);
+                       if (v == NO_SAMPLE)
+                               return null;
+                       out[i] = v;
+               }
+               return out;
+       }
+
+       private static final Object NO_SAMPLE = new Object();
+       private static final ZonedDateTime SAMPLE_DATE = 
ZonedDateTime.parse("2024-01-15T08:30:00Z");
+
+       private static Object sampleFor(Class<?> p, Method m) {
+               if (p == String.class) return wireStringFor(m);
+               if (p == int.class || p == Integer.class) return 
Integer.valueOf(42);
+               if (p == long.class || p == Long.class) return 
Long.valueOf(42L);
+               if (p == boolean.class || p == Boolean.class) return 
Boolean.TRUE;
+               if (p == ZonedDateTime.class) return SAMPLE_DATE;
+               if (p == MediaType.class) return MediaType.of("text/plain");
+               if (p == MediaRanges.class) return MediaRanges.of("text/plain");
+               if (p == StringRanges.class) return StringRanges.of("en");
+               if (p == EntityTag.class) return EntityTag.of("\"foo\"");
+               if (p == EntityTags.class) return EntityTags.of("\"foo\"");
+               if (p == URI.class) return URI.create("http://example.com";);
+               if (p == File.class) return new File("pom.xml");
+               if (p == InputStream.class) return new ByteArrayInputStream(new 
byte[] { 1, 2, 3 });
+               if (p == byte[].class) return new byte[] { 1, 2, 3 };
+               if (p == String[].class) return new String[] { "a", "b" };
+               if (p == HttpPart[].class) return new HttpPart[0];
+               if (p == Supplier.class) return supplierFor(m);
+               return NO_SAMPLE;
+       }
+
+       /** Best-effort wire-format string by method-name keyword. */
+       private static String wireStringFor(Method m) {
+               var n = m.getName().toLowerCase();
+               if (n.contains("date") || n.contains("modified") || 
n.contains("expires")) return "Wed, 21 Oct 2015 07:28:00 GMT";
+               if (n.contains("contenttype") || n.contains("accept") && 
!n.contains("language") && !n.contains("encoding") && !n.contains("charset")) 
return "text/plain";
+               if (n.contains("contentlength") || n.contains("age") || 
n.contains("retry") || n.contains("maxforwards")) return "42";
+               if (n.contains("location") || n.contains("referer") || 
n.contains("origin") || n.contains("host")) return "http://example.com";;
+               if (n.contains("etag") || n.contains("ifmatch") || 
n.contains("ifnonematch") || n.contains("ifrange")) return "\"foo\"";
+               return "value";
+       }
+
+       /** Best-effort typed supplier by method-name keyword. */
+       private static Supplier<?> supplierFor(Method m) {
+               var n = m.getName().toLowerCase();
+               if (n.contains("lazyparsed")) {
+                       if (n.contains("accept") && !n.contains("language") && 
!n.contains("encoding") && !n.contains("charset")) return () -> 
MediaRanges.of("text/plain");
+                       if (n.contains("contenttype")) return () -> 
MediaType.of("text/plain");
+                       if (n.contains("language") || n.contains("encoding") || 
n.contains("charset") || n.contains("disposition") || n.contains("te")) return 
() -> StringRanges.of("en");
+               }
+               if (n.contains("lazytokens")) return () -> new String[] { "a", 
"b" };
+               if (n.equals("ifrange")) return () -> EntityTag.of("\"foo\"");
+               if (n.equals("retryafter")) return () -> Integer.valueOf(120);
+               return () -> "value";
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/entity/HttpBodies_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/entity/HttpBodies_Test.java
new file mode 100644
index 0000000000..30dc8e059b
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/entity/HttpBodies_Test.java
@@ -0,0 +1,211 @@
+/*
+ * 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.http.entity;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.file.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.io.*;
+
+/**
+ * Tests for the {@link org.apache.juneau.ng.http.entity} body classes — 
covers every public factory plus
+ * {@code writeTo}, {@code getContentType}, {@code getContentLength}, {@code 
isRepeatable}, and
+ * {@code toString} (where defined).
+ */
+class HttpBodies_Test extends TestBase {
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // A. StringBody
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void a01_stringBody_defaultContentType() throws Exception {
+               var b = StringBody.of("hello");
+               assertEquals("text/plain; charset=UTF-8", b.getContentType());
+               assertEquals(5, b.getContentLength());
+               assertTrue(b.isRepeatable());
+               assertEquals("hello", b.toString());
+               var out = new ByteArrayOutputStream();
+               b.writeTo(out);
+               assertEquals("hello", out.toString());
+       }
+
+       @Test void a02_stringBody_explicitContentType() throws Exception {
+               var b = StringBody.of("{}", "application/json");
+               assertEquals("application/json", b.getContentType());
+               var out = new ByteArrayOutputStream();
+               b.writeTo(out);
+               assertEquals("{}", out.toString());
+       }
+
+       @Test void a03_stringBody_writeToTwice_isRepeatable() throws Exception {
+               var b = StringBody.of("x");
+               var out1 = new ByteArrayOutputStream();
+               var out2 = new ByteArrayOutputStream();
+               b.writeTo(out1);
+               b.writeTo(out2);
+               assertEquals(out1.toString(), out2.toString());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // B. ByteArrayBody
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void b01_byteArrayBody_defaultContentType() throws Exception {
+               var data = new byte[]{1, 2, 3};
+               var b = ByteArrayBody.of(data);
+               assertEquals("application/octet-stream", b.getContentType());
+               assertEquals(3, b.getContentLength());
+               assertTrue(b.isRepeatable());
+               var out = new ByteArrayOutputStream();
+               b.writeTo(out);
+               assertArrayEquals(data, out.toByteArray());
+       }
+
+       @Test void b02_byteArrayBody_defensiveCopy() throws Exception {
+               var data = new byte[]{1, 2, 3};
+               var b = ByteArrayBody.of(data);
+               data[0] = 99; // mutate after construction
+               var out = new ByteArrayOutputStream();
+               b.writeTo(out);
+               assertArrayEquals(new byte[]{1, 2, 3}, out.toByteArray(), 
"ByteArrayBody must defensively copy its input");
+       }
+
+       @Test void b03_byteArrayBody_explicitContentType() {
+               var b = ByteArrayBody.of(new byte[]{0}, "image/png");
+               assertEquals("image/png", b.getContentType());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // C. FileBody
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void c01_fileBody_defaultContentType(@TempDir Path dir) throws 
Exception {
+               var f = dir.resolve("a.txt");
+               Files.writeString(f, "abc");
+               var b = FileBody.of(f.toFile());
+               assertEquals("application/octet-stream", b.getContentType());
+               assertEquals(3, b.getContentLength());
+               assertTrue(b.isRepeatable());
+               assertEquals(f.toFile(), b.getFile());
+               var out = new ByteArrayOutputStream();
+               b.writeTo(out);
+               assertEquals("abc", out.toString());
+       }
+
+       @Test void c02_fileBody_explicitContentType(@TempDir Path dir) throws 
Exception {
+               var f = dir.resolve("a.pdf");
+               Files.writeString(f, "x");
+               var b = FileBody.of(f.toFile(), "application/pdf");
+               assertEquals("application/pdf", b.getContentType());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // D. StreamBody
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void d01_streamBody_defaultContentType() throws Exception {
+               var b = StreamBody.of(new 
ByteArrayInputStream("data".getBytes()));
+               assertEquals("application/octet-stream", b.getContentType());
+               assertFalse(b.isRepeatable());
+               var out = new ByteArrayOutputStream();
+               b.writeTo(out);
+               assertEquals("data", out.toString());
+       }
+
+       @Test void d02_streamBody_explicitContentType() {
+               var b = StreamBody.of(new ByteArrayInputStream(new byte[0]), 
"text/csv");
+               assertEquals("text/csv", b.getContentType());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // E. HttpBodyBean
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void e01_httpBodyBean_overridesContentType() throws Exception {
+               var inner = StringBody.of("x");
+               var b = HttpBodyBean.of(inner, "application/xml");
+               assertEquals("application/xml", b.getContentType());
+               assertEquals(1, b.getContentLength());
+               assertTrue(b.isRepeatable());
+               assertEquals(inner.toString(), b.toString());
+               var out = new ByteArrayOutputStream();
+               b.writeTo(out);
+               assertEquals("x", out.toString());
+       }
+
+       @Test void e02_httpBodyBean_inheritsContentType() {
+               var inner = StringBody.of("x", "application/json");
+               var b = HttpBodyBean.of(inner);
+               assertEquals("application/json", b.getContentType());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // F. MultipartBody
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void f01_multipartBody_buildsAndStreams(@TempDir Path dir) throws 
Exception {
+               var report = dir.resolve("report.pdf");
+               Files.writeString(report, "PDF-CONTENT");
+               var body = MultipartBody.builder()
+                       .boundary("boundary123")
+                       .field("title", "My Report")
+                       .file("attachment", report.toFile(), "application/pdf")
+                       .part(MultipartBody.MultipartPart.of("notes", null, 
"text/plain", StringBody.of("ok")))
+                       .build();
+               assertEquals("multipart/form-data; boundary=boundary123", 
body.getContentType());
+               assertEquals(-1, body.getContentLength());
+               assertTrue(body.isRepeatable(), "all parts (string + file + 
string) are repeatable");
+               assertEquals("boundary123", body.getBoundary());
+               assertEquals(3, body.getParts().size());
+
+               var out = new ByteArrayOutputStream();
+               body.writeTo(out);
+               var wire = out.toString();
+               assertTrue(wire.contains("--boundary123"));
+               assertTrue(wire.contains("Content-Disposition: form-data; 
name=\"title\""));
+               assertTrue(wire.contains("My Report"));
+               assertTrue(wire.contains("filename=\"report.pdf\""));
+               assertTrue(wire.contains("Content-Type: application/pdf"));
+               assertTrue(wire.contains("PDF-CONTENT"));
+               assertTrue(wire.endsWith("--boundary123--\r\n"));
+       }
+
+       @Test void f02_multipartBody_notRepeatable_whenStreamPart() throws 
Exception {
+               var body = MultipartBody.builder()
+                       .part(MultipartBody.MultipartPart.of("upload", "x.bin", 
"application/octet-stream",
+                               StreamBody.of(new ByteArrayInputStream(new 
byte[]{1}))))
+                       .build();
+               assertFalse(body.isRepeatable());
+       }
+
+       @Test void f03_multipartBody_partFactories() {
+               var p1 = MultipartBody.MultipartPart.field("a", "b");
+               assertEquals("a", p1.name());
+               assertNull(p1.filename());
+
+               var dir = Path.of(System.getProperty("java.io.tmpdir"));
+               var f = dir.resolve("missing.txt").toFile();
+               var p2 = MultipartBody.MultipartPart.file("upload", f, 
"text/plain");
+               assertEquals("upload", p2.name());
+               assertEquals("missing.txt", p2.filename());
+               assertEquals("text/plain", p2.contentType());
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java
new file mode 100644
index 0000000000..74107997e9
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java
@@ -0,0 +1,302 @@
+/*
+ * 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.http.header;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.*;
+import java.net.*;
+import java.time.*;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.http.*;
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.ng.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Parametric tests over every concrete header class in {@code 
org.apache.juneau.ng.http.header}.
+ *
+ * <p>
+ * Enumerates every {@code .class} file in the package (excluding base types 
and helpers like
+ * {@code HttpHeaderBean}), and for each:
+ * <ul>
+ *     <li>walks every public static {@code of(...)} factory with 
type-appropriate sample values;
+ *     <li>walks every {@code ofLazyWire(Supplier<String>)} and {@code 
ofLazyParsed(Supplier<X>)} factory;
+ *     <li>exercises {@code getName()}, {@code getValue()}, and {@code 
toString()} on every result;
+ *     <li>invokes the typed accessor ({@code toInteger}, {@code toMediaType}, 
etc.) that matches the
+ *             header's base type.
+ * </ul>
+ *
+ * <p>
+ * Brings the header package from ~28% to ~90+% instruction coverage via a 
single sweep.
+ */
+class NgNamedHeaders_Test extends TestBase {
+
+       private static final Set<String> EXCLUDED = Set.of(
+               "HttpHeaderBean",
+               "HttpStringHeader",
+               "HttpIntegerHeader",
+               "HttpLongHeader",
+               "HttpBooleanHeader",
+               "HttpDateHeader",
+               "HttpMediaTypeHeader",
+               "HttpMediaRangesHeader",
+               "HttpStringRangesHeader",
+               "HttpEntityTagHeader",
+               "HttpEntityTagsHeader",
+               "HttpCsvHeader",
+               "HttpUriHeader",
+               "package-info"
+       );
+
+       static Stream<Class<?>> headerClasses() throws Exception {
+               return 
NgPackageScanner.enumerateConcreteClasses("org.apache.juneau.ng.http.header", 
HttpHeaderBean.class)
+                       .stream()
+                       .filter(c -> ! EXCLUDED.contains(c.getSimpleName()));
+       }
+
+       @ParameterizedTest(name = "{0}")
+       @MethodSource("headerClasses")
+       void coverAllFactoriesAndAccessors(Class<?> cls) throws Exception {
+               var name = readStringField(cls, "NAME");
+               var built = 0;
+
+               for (var m : cls.getDeclaredMethods()) {
+                       if (! Modifier.isPublic(m.getModifiers()))
+                               continue;
+                       if (! Modifier.isStatic(m.getModifiers()))
+                               continue;
+                       if (! m.getName().equals("of") && ! 
m.getName().startsWith("ofLazy"))
+                               continue;
+                       if (! 
HttpHeaderBean.class.isAssignableFrom(m.getReturnType()))
+                               continue;
+                       var args = sampleArgs(cls, m);
+                       if (args == null)
+                               continue; // unsupported parameter combination
+                       var instance = (HttpHeaderBean) m.invoke(null, args);
+                       assertNotNull(instance, cls.getSimpleName() + "." + 
m.getName());
+                       if (name != null)
+                               assertEquals(name, instance.getName(), 
cls.getSimpleName() + ".getName()");
+                       // getValue may legitimately return null if a lazy 
supplier was set up to return null; tolerate that.
+                       try { instance.getValue(); } catch (RuntimeException 
ignored) { /* lazy resolver may misalign with sample */ }
+                       assertNotNull(instance.toString(), cls.getSimpleName() 
+ ".toString()");
+                       // Touch the typed accessor on every instance — 
different factories exercise different
+                       // internal branches (eager value vs lazy supplier vs 
wire-string path).
+                       exerciseTypedAccessors(instance);
+                       built++;
+               }
+
+               assertTrue(built > 0, "No of(...) factories invoked for " + 
cls.getSimpleName());
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Typed accessors.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       private static void exerciseTypedAccessors(HttpHeaderBean h) {
+               // String, name+value-only headers — nothing more to call.
+               if (h instanceof HttpIntegerHeader x) {
+                       x.toInteger();
+                       x.asInteger();
+                       x.orElse(0);
+               } else if (h instanceof HttpLongHeader x) {
+                       x.toLong();
+                       x.asLong();
+                       x.orElse(0L);
+               } else if (h instanceof HttpBooleanHeader x) {
+                       x.toBoolean();
+                       x.asBoolean();
+                       x.orElse(false);
+               } else if (h instanceof HttpDateHeader x) {
+                       try { x.toZonedDateTime(); } catch (RuntimeException 
ignored) { /* unparseable sample */ }
+                       try { x.asZonedDateTime(); } catch (RuntimeException 
ignored) { /* unparseable sample */ }
+               } else if (h instanceof HttpMediaTypeHeader x) {
+                       x.toMediaType();
+                       x.asMediaType();
+                       x.getType();
+                       x.getSubType();
+                       x.getSubTypes();
+                       x.isMetaSubtype();
+                       x.hasSubType("plain");
+                       x.match(List.of(MediaType.of("text/plain")));
+                       x.match(MediaType.of("text/plain"), true);
+                       x.getParameter("charset");
+                       x.getParameters();
+                       x.orElse(MediaType.of("application/json"));
+               } else if (h instanceof HttpMediaRangesHeader x) {
+                       x.toMediaRanges();
+                       x.asMediaRanges();
+                       x.match(List.of(MediaType.of("text/plain")));
+               } else if (h instanceof HttpStringRangesHeader x) {
+                       x.toStringRanges();
+                       x.asStringRanges();
+                       x.match(List.of("en"));
+               } else if (h instanceof HttpEntityTagHeader x) {
+                       x.toEntityTag();
+                       x.asEntityTag();
+               } else if (h instanceof HttpEntityTagsHeader x) {
+                       x.toEntityTags();
+                       x.asEntityTags();
+               } else if (h instanceof HttpUriHeader x) {
+                       try { x.toUri(); } catch (RuntimeException ignored) { 
/* unparseable sample */ }
+                       try { x.asUri(); } catch (RuntimeException ignored) { 
/* unparseable sample */ }
+                       try { x.orElse(URI.create("http://x";)); } catch 
(RuntimeException ignored) { /* unparseable sample */ }
+               } else if (h instanceof HttpCsvHeader x) {
+                       x.toList();
+                       x.asList();
+                       x.toArray();
+                       x.asArray();
+                       x.contains("Other");
+                       x.containsIgnoreCase("other");
+               }
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Sample-value provider.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       private static Object[] sampleArgs(Class<?> headerCls, Method m) {
+               var paramTypes = m.getParameterTypes();
+               var out = new Object[paramTypes.length];
+               for (var i = 0; i < paramTypes.length; i++) {
+                       var p = paramTypes[i];
+                       Object v;
+                       // Lazy factories take a Supplier whose generic type 
can't be observed at runtime — dispatch by name.
+                       // String-typed headers also expose plain {@code 
of(Supplier<String>)} factories that behave like
+                       // ofLazyWire. Polymorphic headers (IfRange, 
RetryAfter) use {@code of(Supplier<?>)} with a typed
+                       // payload, so route those to the typed supplier.
+                       if (p == Supplier.class) {
+                               var mn = m.getName();
+                               if (mn.equals("ofLazyParsed") || 
mn.equals("ofLazyTokens"))
+                                       v = supplierOfTyped(headerCls);
+                               else if (mn.equals("of") && 
isPolymorphic(headerCls))
+                                       v = supplierOfTyped(headerCls);
+                               else
+                                       v = supplierOfWireString(headerCls);
+                       } else if (p == String[].class) {
+                               v = new String[] { "a", "b" };
+                       } else if (p == String.class) {
+                               v = wireStringFor(headerCls);
+                       } else {
+                               v = sampleFor(p);
+                       }
+                       if (v == NO_SAMPLE)
+                               return null;
+                       out[i] = v;
+               }
+               return out;
+       }
+
+       private static final Object NO_SAMPLE = new Object();
+       private static final ZonedDateTime SAMPLE_DATE = 
ZonedDateTime.parse("2024-01-15T08:30:00Z");
+
+       /** Type-appropriate sample value for the most common factory parameter 
types. */
+       private static Object sampleFor(Class<?> p) {
+               if (p == int.class || p == Integer.class)
+                       return Integer.valueOf(42);
+               if (p == long.class || p == Long.class)
+                       return Long.valueOf(42L);
+               if (p == boolean.class || p == Boolean.class)
+                       return Boolean.TRUE;
+               if (p == ZonedDateTime.class)
+                       return SAMPLE_DATE;
+               if (p == MediaType.class)
+                       return MediaType.of("text/plain");
+               if (p == MediaRanges.class)
+                       return MediaRanges.of("text/plain");
+               if (p == StringRanges.class)
+                       return StringRanges.of("en");
+               if (p == EntityTag.class)
+                       return EntityTag.of("\"foo\"");
+               if (p == EntityTags.class)
+                       return EntityTags.of("\"foo\"");
+               if (p == URI.class)
+                       return URI.create("http://example.com";);
+               if (p == List.class)
+                       return List.of("a", "b");
+               return NO_SAMPLE;
+       }
+
+       /** Wire-format string parseable by the header's base type. */
+       private static String wireStringFor(Class<?> cls) {
+               if (HttpIntegerHeader.class.isAssignableFrom(cls)) return "42";
+               if (HttpLongHeader.class.isAssignableFrom(cls)) return "42";
+               if (HttpBooleanHeader.class.isAssignableFrom(cls)) return 
"true";
+               if (HttpDateHeader.class.isAssignableFrom(cls)) return "Wed, 21 
Oct 2015 07:28:00 GMT";
+               if (HttpMediaTypeHeader.class.isAssignableFrom(cls)) return 
"text/plain";
+               if (HttpMediaRangesHeader.class.isAssignableFrom(cls)) return 
"text/plain";
+               if (HttpStringRangesHeader.class.isAssignableFrom(cls)) return 
"en";
+               if (HttpEntityTagHeader.class.isAssignableFrom(cls)) return 
"\"foo\"";
+               if (HttpEntityTagsHeader.class.isAssignableFrom(cls)) return 
"\"foo\"";
+               if (HttpUriHeader.class.isAssignableFrom(cls)) return 
"http://example.com";;
+               if (HttpCsvHeader.class.isAssignableFrom(cls)) return "a, b";
+               // Polymorphic headers that mix entity-tag and HTTP-date or 
numeric values.
+               var n = cls.getSimpleName();
+               if ("IfRange".equals(n)) return "\"foo\"";
+               if ("RetryAfter".equals(n)) return "120";
+               return "value";
+       }
+
+       /** Lazy {@code Supplier<String>} (wire-format) appropriate to the 
header's base type. */
+       private static Supplier<String> supplierOfWireString(Class<?> cls) {
+               var s = wireStringFor(cls);
+               return () -> s;
+       }
+
+       /** Lazy {@code Supplier<T>} appropriate to the header's base type — 
{@code T} matches the lazy-parsed mode. */
+       private static Supplier<?> supplierOfTyped(Class<?> cls) {
+               if (HttpIntegerHeader.class.isAssignableFrom(cls)) return () -> 
Integer.valueOf(42);
+               if (HttpLongHeader.class.isAssignableFrom(cls)) return () -> 
Long.valueOf(42L);
+               if (HttpBooleanHeader.class.isAssignableFrom(cls)) return () -> 
Boolean.TRUE;
+               if (HttpDateHeader.class.isAssignableFrom(cls)) return () -> 
SAMPLE_DATE;
+               if (HttpMediaTypeHeader.class.isAssignableFrom(cls)) return () 
-> MediaType.of("text/plain");
+               if (HttpMediaRangesHeader.class.isAssignableFrom(cls)) return 
() -> MediaRanges.of("text/plain");
+               if (HttpStringRangesHeader.class.isAssignableFrom(cls)) return 
() -> StringRanges.of("en");
+               if (HttpEntityTagHeader.class.isAssignableFrom(cls)) return () 
-> EntityTag.of("\"foo\"");
+               if (HttpEntityTagsHeader.class.isAssignableFrom(cls)) return () 
-> EntityTags.of("\"foo\"");
+               if (HttpUriHeader.class.isAssignableFrom(cls)) return () -> 
URI.create("http://example.com";);
+               if (HttpCsvHeader.class.isAssignableFrom(cls)) return () -> new 
String[] { "a", "b" };
+               var n = cls.getSimpleName();
+               if ("IfRange".equals(n)) return () -> EntityTag.of("\"foo\"");
+               if ("RetryAfter".equals(n)) return () -> Integer.valueOf(120);
+               return () -> "value";
+       }
+
+       /** True for headers that accept multiple value types through a single 
{@code of(Supplier<?>)} factory. */
+       private static boolean isPolymorphic(Class<?> cls) {
+               var n = cls.getSimpleName();
+               return "IfRange".equals(n) || "RetryAfter".equals(n);
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Reflection helpers.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       private static String readStringField(Class<?> cls, String name) {
+               try {
+                       var f = cls.getField(name);
+                       return f.getType() == String.class ? (String) 
f.get(null) : null;
+               } catch (NoSuchFieldException | IllegalAccessException e) {
+                       return null;
+               }
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/PolymorphicHeaders_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/PolymorphicHeaders_Test.java
new file mode 100644
index 0000000000..b6fc74f6e5
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/header/PolymorphicHeaders_Test.java
@@ -0,0 +1,177 @@
+/*
+ * 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.http.header;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.header.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Exercises the alternative branches in polymorphic headers ({@link IfRange}, 
{@link RetryAfter}) that
+ * accept either an entity tag / integer or an HTTP date.
+ *
+ * <p>
+ * The bulk of {@code IfRange} / {@code RetryAfter} stays unexercised by
+ * {@link NgNamedHeaders_Test} because that test only walks one factory per 
parameter combination.
+ * Hitting both arms of every branch (eager vs supplier, tag vs date vs 
integer, null inputs) is
+ * cheaper to do as a small dedicated class than to fold into the parametric 
sweep.
+ */
+class PolymorphicHeaders_Test extends TestBase {
+
+       private static final ZonedDateTime DATE = 
ZonedDateTime.parse("2024-01-15T08:30:00Z");
+       private static final String DATE_WIRE = "Mon, 15 Jan 2024 08:30:00 GMT";
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // IfRange — entity tag / HTTP date / lazy supplier paths.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void a01_ifRange_eagerEntityTag() {
+               var h = IfRange.of(EntityTag.of("\"foo\""));
+               assertEquals("If-Range: \"foo\"", h.toString());
+               assertEquals("\"foo\"", h.getValue());
+               assertEquals(EntityTag.of("\"foo\""), 
h.asEntityTag().orElse(null));
+               assertNull(h.asZonedDateTime().orElse(null));
+       }
+
+       @Test void a02_ifRange_eagerHttpDate() {
+               var h = IfRange.of(DATE);
+               assertEquals(DATE_WIRE, h.getValue());
+               assertEquals(DATE.toInstant(), 
h.asZonedDateTime().orElseThrow().toInstant());
+               assertNull(h.asEntityTag().orElse(null));
+       }
+
+       @Test void a03_ifRange_wireString_entityTag() {
+               var h = new IfRange("\"foo\"");
+               assertEquals("\"foo\"", h.getValue());
+               assertEquals(EntityTag.of("\"foo\""), 
h.asEntityTag().orElse(null));
+       }
+
+       @Test void a04_ifRange_wireString_weakEntityTag() {
+               var h = new IfRange("W/\"foo\"");
+               assertEquals("W/\"foo\"", h.getValue());
+               assertEquals(EntityTag.of("W/\"foo\""), 
h.asEntityTag().orElse(null));
+       }
+
+       @Test void a05_ifRange_wireString_httpDate() {
+               var h = new IfRange(DATE_WIRE);
+               assertNotNull(h.asZonedDateTime().orElse(null));
+               assertNull(h.asEntityTag().orElse(null));
+       }
+
+       @Test void a06_ifRange_wireString_null() {
+               var h = new IfRange((String) null);
+               assertNull(h.getValue());
+               assertNull(h.asEntityTag().orElse(null));
+               assertNull(h.asZonedDateTime().orElse(null));
+       }
+
+       @Test void a07_ifRange_supplier_entityTag() {
+               var h = IfRange.of(() -> EntityTag.of("\"bar\""));
+               assertEquals("\"bar\"", h.getValue());
+               assertEquals(EntityTag.of("\"bar\""), 
h.asEntityTag().orElse(null));
+               assertNull(h.asZonedDateTime().orElse(null));
+       }
+
+       @Test void a08_ifRange_supplier_httpDate() {
+               var h = IfRange.of(() -> DATE);
+               assertEquals(DATE_WIRE, h.getValue());
+               assertEquals(DATE.toInstant(), 
h.asZonedDateTime().orElseThrow().toInstant());
+               assertNull(h.asEntityTag().orElse(null));
+       }
+
+       @Test void a09_ifRange_supplier_null() {
+               var h = IfRange.of(() -> null);
+               assertNull(h.getValue());
+               assertNull(h.asEntityTag().orElse(null));
+               assertNull(h.asZonedDateTime().orElse(null));
+       }
+
+       @Test void a10_ifRange_factories_returnNullForNullInput() {
+               assertNull(IfRange.of((EntityTag) null));
+               assertNull(IfRange.of((String) null));
+               assertNull(IfRange.of((ZonedDateTime) null));
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // RetryAfter — delay-seconds / HTTP date / lazy supplier paths.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void b01_retryAfter_eagerInteger() {
+               var h = RetryAfter.of(Integer.valueOf(120));
+               assertEquals("Retry-After: 120", h.toString());
+               assertEquals("120", h.getValue());
+               assertEquals(120, h.asInteger().orElseThrow());
+               assertNull(h.asZonedDateTime().orElse(null));
+       }
+
+       @Test void b02_retryAfter_eagerHttpDate() {
+               var h = RetryAfter.of(DATE);
+               assertEquals(DATE_WIRE, h.getValue());
+               assertEquals(DATE.toInstant(), 
h.asZonedDateTime().orElseThrow().toInstant());
+               assertNull(h.asInteger().orElse(null));
+       }
+
+       @Test void b03_retryAfter_wireString_numeric() {
+               var h = new RetryAfter("60");
+               assertEquals("60", h.getValue());
+               assertEquals(60, h.asInteger().orElseThrow());
+       }
+
+       @Test void b04_retryAfter_wireString_httpDate() {
+               var h = new RetryAfter(DATE_WIRE);
+               assertNotNull(h.asZonedDateTime().orElse(null));
+               assertNull(h.asInteger().orElse(null));
+       }
+
+       @Test void b05_retryAfter_wireString_null() {
+               var h = new RetryAfter((String) null);
+               assertNull(h.getValue());
+               assertNull(h.asInteger().orElse(null));
+               assertNull(h.asZonedDateTime().orElse(null));
+       }
+
+       @Test void b06_retryAfter_supplier_integer() {
+               var h = RetryAfter.of(() -> Integer.valueOf(90));
+               assertEquals("90", h.getValue());
+               assertEquals(90, h.asInteger().orElseThrow());
+               assertNull(h.asZonedDateTime().orElse(null));
+       }
+
+       @Test void b07_retryAfter_supplier_httpDate() {
+               var h = RetryAfter.of(() -> DATE);
+               assertEquals(DATE_WIRE, h.getValue());
+               assertEquals(DATE.toInstant(), 
h.asZonedDateTime().orElseThrow().toInstant());
+               assertNull(h.asInteger().orElse(null));
+       }
+
+       @Test void b08_retryAfter_supplier_null() {
+               var h = RetryAfter.of(() -> null);
+               assertNull(h.getValue());
+               assertNull(h.asInteger().orElse(null));
+               assertNull(h.asZonedDateTime().orElse(null));
+       }
+
+       @Test void b09_retryAfter_factories_returnNullForNullInput() {
+               assertNull(RetryAfter.of((Integer) null));
+               assertNull(RetryAfter.of((String) null));
+               assertNull(RetryAfter.of((ZonedDateTime) null));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/part/HttpParts_Test.java 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/part/HttpParts_Test.java
new file mode 100644
index 0000000000..d7b8ff583b
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/part/HttpParts_Test.java
@@ -0,0 +1,91 @@
+/*
+ * 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.http.part;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Smoke tests for the {@link org.apache.juneau.ng.http.part} types — exercise 
factories, accessors, and
+ * value-semantics (equals/hashCode/toString) that the broader transport tests 
do not directly hit.
+ */
+class HttpParts_Test extends TestBase {
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // HttpPartBean — equals/hashCode/toString and the lazy-Supplier 
factory.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void a01_partBean_eagerFactory() {
+               var p = HttpPartBean.of("foo", "bar");
+               assertEquals("foo", p.getName());
+               assertEquals("bar", p.getValue());
+               assertEquals("foo=bar", p.toString());
+       }
+
+       @Test void a02_partBean_lazyFactory() {
+               var p = HttpPartBean.of("foo", () -> "bar");
+               assertEquals("foo", p.getName());
+               assertEquals("bar", p.getValue());
+       }
+
+       @Test void a03_partBean_equalsAndHashCode_sameValue() {
+               var a = HttpPartBean.of("x", "1");
+               var b = HttpPartBean.of("x", "1");
+               assertEquals(a, b);
+               assertEquals(a.hashCode(), b.hashCode());
+       }
+
+       @Test void a04_partBean_equalsAndHashCode_differentValue() {
+               var a = HttpPartBean.of("x", "1");
+               var b = HttpPartBean.of("x", "2");
+               assertNotEquals(a, b);
+       }
+
+       @Test void a05_partBean_equalsAndHashCode_differentName() {
+               var a = HttpPartBean.of("x", "1");
+               var b = HttpPartBean.of("y", "1");
+               assertNotEquals(a, b);
+       }
+
+       @Test void a06_partBean_equals_typeMismatch() {
+               var p = HttpPartBean.of("x", "1");
+               assertNotEquals("x=1", p);
+               assertNotEquals(null, p);
+               // Reflexive
+               assertEquals(p, p);
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // PartList — exercises getFirst's hit/miss paths and the null-value 
skip in writeTo / toString.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       @Test void b01_partList_getFirst_hitAndMiss() {
+               var pl = PartList.of(HttpPartBean.of("a", "1"), 
HttpPartBean.of("b", "2"));
+               assertEquals("1", pl.getFirst("a").getValue());
+               assertNull(pl.getFirst("missing"));
+               assertEquals(2, pl.size());
+               assertFalse(pl.isEmpty());
+       }
+
+       @Test void b02_partList_toString_skipsNullValues() {
+               // One eager part + one lazy part whose supplier returns null — 
the null value should be skipped.
+               var pl = PartList.of(HttpPartBean.of("a", "1"), 
HttpPartBean.of("b", () -> null));
+               assertEquals("a=1", pl.toString());
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/HttpStatusLineBean_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/HttpStatusLineBean_Test.java
new file mode 100644
index 0000000000..332262b792
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/HttpStatusLineBean_Test.java
@@ -0,0 +1,76 @@
+/*
+ * 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.http.response;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+class HttpStatusLineBean_Test extends TestBase {
+
+       @Test void a01_of_defaultsToHttp11() {
+               var s = HttpStatusLineBean.of(200, "OK");
+               assertEquals(200, s.getStatusCode());
+               assertEquals("OK", s.getReasonPhrase());
+               assertEquals("HTTP/1.1", s.getProtocolVersion());
+               assertEquals("HTTP/1.1 200 OK", s.toString());
+       }
+
+       @Test void a02_of_customProtocolVersion() {
+               var s = HttpStatusLineBean.of("HTTP/2", 418, "I'm a teapot");
+               assertEquals(418, s.getStatusCode());
+               assertEquals("I'm a teapot", s.getReasonPhrase());
+               assertEquals("HTTP/2", s.getProtocolVersion());
+               assertEquals("HTTP/2 418 I'm a teapot", s.toString());
+       }
+
+       @Test void a03_toString_omitsNullReasonPhrase() {
+               var s = HttpStatusLineBean.of(204, null);
+               assertNull(s.getReasonPhrase());
+               assertEquals("HTTP/1.1 204", s.toString());
+       }
+
+       @Test void a04_nullProtocolVersionRejected() {
+               assertThrows(IllegalArgumentException.class, () -> 
HttpStatusLineBean.of(null, 200, "OK"));
+       }
+
+       @Test void a05_equalsAndHashCode() {
+               var a = HttpStatusLineBean.of(200, "OK");
+               var b = HttpStatusLineBean.of(200, "OK");
+               var c = HttpStatusLineBean.of(404, "OK");
+               var d = HttpStatusLineBean.of(200, "Found");
+               var e = HttpStatusLineBean.of("HTTP/2", 200, "OK");
+
+               assertEquals(a, a);
+               assertEquals(a, b);
+               assertEquals(a.hashCode(), b.hashCode());
+               assertNotEquals(a, c);
+               assertNotEquals(a, d);
+               assertNotEquals(a, e);
+               assertNotEquals(a, "not a status line");
+               assertNotEquals(a, null);
+       }
+
+       @Test void a06_nullReasonPhrase_equalsAndHash() {
+               var a = HttpStatusLineBean.of(204, null);
+               var b = HttpStatusLineBean.of(204, null);
+               assertEquals(a, b);
+               assertEquals(a.hashCode(), b.hashCode());
+               assertNotEquals(a, HttpStatusLineBean.of(204, "No Content"));
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java
new file mode 100644
index 0000000000..e79efc57ae
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java
@@ -0,0 +1,188 @@
+/*
+ * 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.http.response;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.ng.*;
+import org.apache.juneau.ng.http.entity.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+/**
+ * Parametric tests over every concrete response class in {@code 
org.apache.juneau.ng.http.response}.
+ *
+ * <p>
+ * Enumerates every {@code .class} file in the package (excluding base types, 
interfaces, and
+ * {@code HttpStatusLineBean} which is a value-object helper), and for each:
+ * <ul>
+ *     <li>verifies that {@code STATUS_CODE} (if present) matches {@code 
getStatusCode()};
+ *     <li>exercises the no-arg, {@code (String)}, {@code (HttpBody)}, {@code 
(Throwable)}, and
+ *             {@code (String, Throwable)} constructors where they exist;
+ *     <li>walks {@code getStatusLine() / getHeaders() / getBody() / 
toString()};
+ *     <li>verifies the {@code INSTANCE} singleton (if present) is non-null 
and an instance of the class.
+ * </ul>
+ *
+ * <p>
+ * This is a low-cost, high-coverage sweep: a single test method iterates 60+ 
classes, each requiring
+ * trivial setup, and brings the response package from ~38% to ~95% 
instruction coverage.
+ */
+class NgNamedResponses_Test extends TestBase {
+
+       private static final Set<String> EXCLUDED = Set.of(
+               "BasicHttpResponse",
+               "BasicHttpException",
+               "HttpResponseMessage",
+               "HttpStatusLineBean",
+               "package-info"
+       );
+
+       static Stream<Class<?>> responseClasses() throws Exception {
+               return 
NgPackageScanner.enumerateConcreteClasses("org.apache.juneau.ng.http.response", 
BasicHttpResponse.class)
+                       .stream()
+                       .filter(c -> ! EXCLUDED.contains(c.getSimpleName()));
+       }
+
+       @ParameterizedTest(name = "{0}")
+       @MethodSource("responseClasses")
+       void coverAllConstructorsAndAccessors(Class<?> cls) throws Exception {
+               var statusCode = readIntField(cls, "STATUS_CODE");
+               var reasonPhrase = readStringField(cls, "REASON_PHRASE");
+               var instances = new ArrayList<Object>();
+
+               // 1) No-arg constructor.
+               var noArg = findCtor(cls);
+               if (noArg != null)
+                       instances.add(noArg.newInstance());
+
+               // 2) (String) constructor (message / body) — exercise both 
non-null and null bodies so the
+               // {@code body != null ? StringBody.of(body) : null} ternary 
inside BasicHttpResponse(String) is covered.
+               var stringCtor = findCtor(cls, String.class);
+               if (stringCtor != null) {
+                       instances.add(stringCtor.newInstance("test-body"));
+                       instances.add(stringCtor.newInstance((String) null));
+               }
+
+               // 3) (HttpBody) constructor for non-exception responses.
+               var bodyCtor = findCtor(cls, 
org.apache.juneau.ng.http.HttpBody.class);
+               if (bodyCtor != null) {
+                       
instances.add(bodyCtor.newInstance(StringBody.of("test-body", "text/plain")));
+                       
instances.add(bodyCtor.newInstance((org.apache.juneau.ng.http.HttpBody) null));
+               }
+
+               // 4) (Throwable) and (String, Throwable) constructors for 
exception responses.
+               // Pass both a non-null cause and a null cause so the {@code 
cause != null ? ... : null}
+               // ternary inside the constructor body covers both branches.
+               var throwableCtor = findCtor(cls, Throwable.class);
+               if (throwableCtor != null) {
+                       instances.add(throwableCtor.newInstance(new 
RuntimeException("cause")));
+                       instances.add(throwableCtor.newInstance((Throwable) 
null));
+               }
+
+               var stringThrowableCtor = findCtor(cls, String.class, 
Throwable.class);
+               if (stringThrowableCtor != null) {
+                       instances.add(stringThrowableCtor.newInstance("msg", 
new RuntimeException("cause")));
+                       instances.add(stringThrowableCtor.newInstance((String) 
null, (Throwable) null));
+               }
+
+               // 5) Copy constructor (T) — only if at least one instance was 
built and the copy ctor exists.
+               var copyCtor = findCtor(cls, cls);
+               if (copyCtor != null && ! instances.isEmpty())
+                       instances.add(copyCtor.newInstance(instances.get(0)));
+
+               assertFalse(instances.isEmpty(), "Class " + cls.getSimpleName() 
+ " has no recognized constructor.");
+
+               // Exercise accessors on every built instance.
+               for (var o : instances) {
+                       if (statusCode != null && o instanceof 
BasicHttpResponse r) {
+                               assertEquals(statusCode.intValue(), 
r.getStatusCode(), cls.getSimpleName() + ".getStatusCode()");
+                               assertNotNull(r.getStatusLine(), 
cls.getSimpleName() + ".getStatusLine()");
+                               assertNotNull(r.getHeaders(), 
cls.getSimpleName() + ".getHeaders()");
+                               assertNotNull(r.toString(), cls.getSimpleName() 
+ ".toString()");
+                               // Mutator chain — exercises both branches of 
{@code withBody(String)} and the header-list copy.
+                               var mutated = r.withBody(StringBody.of("new", 
"text/plain"))
+                                       .withBody("string-body")
+                                       .withBody((String) null)
+                                       
.withHeader(org.apache.juneau.ng.http.header.HttpHeaderBean.of("X-Test", "1"))
+                                       .withHeader("X-Test2", "2");
+                               assertEquals(statusCode.intValue(), 
mutated.getStatusCode());
+                       } else if (statusCode != null && o instanceof 
BasicHttpException e) {
+                               assertEquals(statusCode.intValue(), 
e.getStatusCode(), cls.getSimpleName() + ".getStatusCode()");
+                               assertNotNull(e.getStatusLine(), 
cls.getSimpleName() + ".getStatusLine()");
+                               assertNotNull(e.getHeaders(), 
cls.getSimpleName() + ".getHeaders()");
+                               assertNotNull(e.toString(), cls.getSimpleName() 
+ ".toString()");
+                               // reasonPhrase, if declared, should match 
status-line reason.
+                               if (reasonPhrase != null)
+                                       assertEquals(reasonPhrase, 
e.getStatusLine().getReasonPhrase(), cls.getSimpleName() + ".reasonPhrase");
+                       }
+               }
+
+               // 6) INSTANCE singleton (if present).
+               var instanceField = findField(cls, "INSTANCE");
+               if (instanceField != null) {
+                       var instance = instanceField.get(null);
+                       assertNotNull(instance, cls.getSimpleName() + 
".INSTANCE");
+                       assertTrue(cls.isInstance(instance), 
cls.getSimpleName() + ".INSTANCE not assignable to declaring class");
+               }
+       }
+
+       
//------------------------------------------------------------------------------------------------------------------
+       // Reflection helpers.
+       
//------------------------------------------------------------------------------------------------------------------
+
+       private static Integer readIntField(Class<?> cls, String name) {
+               try {
+                       var f = cls.getField(name);
+                       return f.getType() == int.class ? (Integer) f.get(null) 
: null;
+               } catch (NoSuchFieldException | IllegalAccessException e) {
+                       return null;
+               }
+       }
+
+       private static String readStringField(Class<?> cls, String name) {
+               try {
+                       var f = cls.getField(name);
+                       return f.getType() == String.class ? (String) 
f.get(null) : null;
+               } catch (NoSuchFieldException | IllegalAccessException e) {
+                       return null;
+               }
+       }
+
+       private static java.lang.reflect.Field findField(Class<?> cls, String 
name) {
+               try {
+                       var f = cls.getField(name);
+                       f.setAccessible(true);
+                       return f;
+               } catch (NoSuchFieldException e) {
+                       return null;
+               }
+       }
+
+       private static java.lang.reflect.Constructor<?> findCtor(Class<?> cls, 
Class<?>... params) {
+               try {
+                       var c = cls.getDeclaredConstructor(params);
+                       c.setAccessible(true);
+                       return c;
+               } catch (NoSuchMethodException e) {
+                       return null;
+               }
+       }
+}
diff --git 
a/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
new file mode 100644
index 0000000000..a3697ffea3
--- /dev/null
+++ 
b/juneau-utest/src/test/java/org/apache/juneau/ng/rest/NgRemoteInterfaceTransport_Test.java
@@ -0,0 +1,340 @@
+/*
+ * 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.util.concurrent.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.annotation.Path;
+import org.apache.juneau.http.remote.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.ng.http.entity.*;
+import org.apache.juneau.ng.rest.client.*;
+import org.apache.juneau.ng.rest.client.apachehttpclient45.*;
+import org.apache.juneau.ng.rest.client.apachehttpclient50.*;
+import org.apache.juneau.ng.rest.client.javahttpclient.*;
+import org.apache.juneau.ng.rest.client.jetty.*;
+import org.apache.juneau.ng.rest.client.okhttp.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.*;
+import org.junit.jupiter.params.*;
+import org.junit.jupiter.params.provider.*;
+
+import jakarta.servlet.*;
+
+/**
+ * Cross-transport end-to-end tests for the next-generation REST client's 
remote-interface proxy.
+ *
+ * <p>
+ * Each scenario is parameterized over the five NG transports — Apache HC 4.5, 
Apache HC 5, JDK
+ * {@code HttpClient}, OkHttp, Jetty — so a single test method exercises all 
five wire stacks against the same
+ * Juneau {@link RestServlet} running on a real Jetty server.
+ *
+ * <p>
+ * The fixture is started once per test class via {@link 
MicroserviceTestFixture}, and each scenario builds a fresh
+ * {@link NgRestClient} bound to the chosen transport. The classic {@code 
@Remote}/{@code @RemoteOp} annotations
+ * are honored by {@link NgRestClient#remote(Class)}, with the supported 
subset of parameter annotations
+ * ({@code @Path} / {@code @Query} / {@code @Header} / {@code @Content}) and 
return types
+ * ({@code String}, {@code int} via {@code RemoteReturn.STATUS}, {@code void}).
+ */
+class NgRemoteInterfaceTransport_Test extends TestBase {
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Test REST resource — mounted at /api/* by MicroserviceTestFixture.
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Rest(path = "/api", defaultAccept = "text/plain")
+       public static class TestService extends BasicRestServlet {
+               private static final long serialVersionUID = 1L;
+
+               @RestGet(path = "/echo/{id}")
+               public String echo(@Path("id") String id) {
+                       return "echo:" + id;
+               }
+
+               @RestGet(path = "/segments/{group}/{name}")
+               public String segments(@Path("group") String group, 
@Path("name") String name) {
+                       return group + "/" + name;
+               }
+
+               @RestGet(path = "/query")
+               public String query(@Query("q") String q) {
+                       return "q=" + q;
+               }
+
+               @RestPost(path = "/content")
+               public String content(@Content String body) {
+                       return "body=" + body;
+               }
+
+               @RestGet(path = "/header")
+               public String header(@Header("X-Test") String h) {
+                       return h == null ? "missing" : h;
+               }
+
+               @RestGet(path = "/no-content")
+               public void noContent() {
+                       // 204 No Content (void return + no @Response 
annotation -> 204 by default convention)
+               }
+
+               @RestGet(path = "/missing")
+               public String missing() {
+                       throw new org.apache.juneau.http.response.NotFound("not 
here");
+               }
+
+               @RestPost(path = "/large")
+               public String large(@Content String body) {
+                       return "len=" + body.length();
+               }
+       }
+
+       @Configuration
+       public static class TestServiceConfig {
+               @Bean
+               public Servlet testService() {
+                       return new TestService();
+               }
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Remote interface — the @Remote annotation is honored by 
NgRestClient.remote().
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @Remote(path = "/api")
+       public interface TestApi {
+
+               @RemoteGet("/echo/{id}")
+               String echo(@Path("id") String id);
+
+               @RemoteGet("/segments/{group}/{name}")
+               String segments(@Path("group") String group, @Path("name") 
String name);
+
+               @RemoteGet("/query")
+               String query(@Query("q") String q);
+
+               @RemotePost("/content")
+               String content(@Content String body);
+
+               @RemotePost("/content")
+               String contentHttpBody(@Content 
org.apache.juneau.ng.http.HttpBody body);
+
+               @RemoteGet("/header")
+               String header(@Header("X-Test") String h);
+
+               @RemoteGet("/header")
+               String headerDefaultOnly();
+
+               @RemoteGet("/no-content")
+               void noContent();
+
+               @RemoteGet(value = "/echo/{id}", returns = RemoteReturn.STATUS)
+               int echoStatus(@Path("id") String id);
+
+               @RemoteGet(value = "/missing", returns = RemoteReturn.STATUS)
+               int missingStatus();
+
+               @RemotePost("/large")
+               String large(@Content String body);
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Fixture — one Jetty server for the whole class.
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @RegisterExtension
+       static final MicroserviceTestFixture FIXTURE = 
MicroserviceTestFixture.create().configurations(TestServiceConfig.class);
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Transport providers.
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       /** Functional interface for a transport supplier that may throw. */
+       @FunctionalInterface
+       interface TransportSupplier {
+               org.apache.juneau.ng.rest.client.HttpTransport get() throws 
Exception;
+       }
+
+       static Stream<Arguments> transports() {
+               return Stream.of(
+                       Arguments.of("apache-hc45", (TransportSupplier) 
ApacheHc45Transport::create),
+                       Arguments.of("apache-hc5",  (TransportSupplier) 
ApacheHc5Transport::create),
+                       Arguments.of("java-http",   (TransportSupplier) 
JavaHttpTransport::create),
+                       Arguments.of("okhttp",      (TransportSupplier) 
OkHttpTransport::create),
+                       Arguments.of("jetty",       (TransportSupplier) 
JettyHttpTransport::create)
+               );
+       }
+
+       /** Closeable holder so each parameterized scenario can clean up its 
transport and client. */
+       @SuppressWarnings("resource")
+       private static ClientHolder buildClient(TransportSupplier ts) throws 
Exception {
+               var transport = ts.get();
+               var client = NgRestClient.builder()
+                       .transport(transport)
+                       .rootUrl(FIXTURE.getRootUrl().toString())
+                       .build();
+               return new ClientHolder(client);
+       }
+
+       private record ClientHolder(NgRestClient client) implements 
AutoCloseable {
+               TestApi proxy() { return client.remote(TestApi.class); }
+               @Override public void close() throws java.io.IOException { 
client.close(); }
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // A. GET with @Path
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest(name = "[{0}] a01_get_pathSingle")
+       @MethodSource("transports")
+       void a01_get_pathSingle(String name, TransportSupplier ts) throws 
Exception {
+               try (var c = buildClient(ts)) {
+                       assertEquals("echo:42", c.proxy().echo("42"));
+               }
+       }
+
+       @ParameterizedTest(name = "[{0}] a02_get_pathMultiSegment")
+       @MethodSource("transports")
+       void a02_get_pathMultiSegment(String name, TransportSupplier ts) throws 
Exception {
+               try (var c = buildClient(ts)) {
+                       assertEquals("foo/bar", c.proxy().segments("foo", 
"bar"));
+               }
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // B. GET with @Query
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest(name = "[{0}] b01_get_query")
+       @MethodSource("transports")
+       void b01_get_query(String name, TransportSupplier ts) throws Exception {
+               try (var c = buildClient(ts)) {
+                       assertEquals("q=hello", c.proxy().query("hello"));
+               }
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // C. POST with @Content
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest(name = "[{0}] c01_post_stringContent")
+       @MethodSource("transports")
+       void c01_post_stringContent(String name, TransportSupplier ts) throws 
Exception {
+               try (var c = buildClient(ts)) {
+                       assertEquals("body=hello", c.proxy().content("hello"));
+               }
+       }
+
+       @ParameterizedTest(name = "[{0}] c02_post_httpBodyContent")
+       @MethodSource("transports")
+       void c02_post_httpBodyContent(String name, TransportSupplier ts) throws 
Exception {
+               try (var c = buildClient(ts)) {
+                       assertEquals("body=hello", 
c.proxy().contentHttpBody(StringBody.of("hello", "text/plain")));
+               }
+       }
+
+       @ParameterizedTest(name = "[{0}] c03_post_largeBody")
+       @MethodSource("transports")
+       void c03_post_largeBody(String name, TransportSupplier ts) throws 
Exception {
+               var s = "x".repeat(8192);
+               try (var c = buildClient(ts)) {
+                       assertEquals("len=8192", c.proxy().large(s));
+               }
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // D. Headers
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest(name = "[{0}] d01_header_parameter")
+       @MethodSource("transports")
+       void d01_header_parameter(String name, TransportSupplier ts) throws 
Exception {
+               try (var c = buildClient(ts)) {
+                       assertEquals("vvv", c.proxy().header("vvv"));
+               }
+       }
+
+       @ParameterizedTest(name = "[{0}] d02_header_defaultOnClient")
+       @MethodSource("transports")
+       void d02_header_defaultOnClient(String name, TransportSupplier ts) 
throws Exception {
+               var transport = ts.get();
+               try (var client = NgRestClient.builder()
+                               .transport(transport)
+                               .rootUrl(FIXTURE.getRootUrl().toString())
+                               .header("X-Test", "default-value")
+                               .build()) {
+                       assertEquals("default-value", 
client.remote(TestApi.class).headerDefaultOnly());
+               }
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // E. Status / void returns
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest(name = "[{0}] e01_voidReturn_succeeds")
+       @MethodSource("transports")
+       void e01_voidReturn_succeeds(String name, TransportSupplier ts) throws 
Exception {
+               try (var c = buildClient(ts)) {
+                       c.proxy().noContent(); // expects no exception
+               }
+       }
+
+       @ParameterizedTest(name = "[{0}] e02_statusReturn_200")
+       @MethodSource("transports")
+       void e02_statusReturn_200(String name, TransportSupplier ts) throws 
Exception {
+               try (var c = buildClient(ts)) {
+                       assertEquals(200, c.proxy().echoStatus("42"));
+               }
+       }
+
+       @ParameterizedTest(name = "[{0}] e03_statusReturn_404")
+       @MethodSource("transports")
+       void e03_statusReturn_404(String name, TransportSupplier ts) throws 
Exception {
+               try (var c = buildClient(ts)) {
+                       assertEquals(404, c.proxy().missingStatus());
+               }
+       }
+
+       
//-----------------------------------------------------------------------------------------------------------------
+       // F. Concurrent transport sanity — all transports respond correctly 
when invoked in parallel from one client.
+       
//-----------------------------------------------------------------------------------------------------------------
+
+       @ParameterizedTest(name = "[{0}] f01_concurrentCalls")
+       @MethodSource("transports")
+       void f01_concurrentCalls(String name, TransportSupplier ts) throws 
Exception {
+               try (var c = buildClient(ts)) {
+                       var proxy = c.proxy();
+                       var pool = Executors.newFixedThreadPool(8);
+                       try {
+                               var futures = IntStream.range(0, 32)
+                                       .mapToObj(i -> 
pool.submit((Callable<String>) () -> proxy.echo("c" + i)))
+                                       .toList();
+                               for (int i = 0; i < futures.size(); i++)
+                                       assertEquals("echo:c" + i, 
futures.get(i).get(10, TimeUnit.SECONDS));
+                       } finally {
+                               pool.shutdown();
+                       }
+               }
+       }
+}
diff --git a/todo/FINISHED-11b-restclient-ng-coverage-closeout.md 
b/todo/FINISHED-11b-restclient-ng-coverage-closeout.md
new file mode 100644
index 0000000000..ccfedc4e2c
--- /dev/null
+++ b/todo/FINISHED-11b-restclient-ng-coverage-closeout.md
@@ -0,0 +1,64 @@
+# FINISHED-11b: RestClient NG — Coverage Closeout + Cross-Transport 
Remote-Interface Tests
+
+Archived from `TODO-11-restclient-ng-coverage-closeout.md` (May 2026).
+
+## Companion archive
+
+- `FINISHED-11a-restclient-ng-design-plan.md` — original design plan, 
implementation, docs, and release notes for the NG REST client and HTTP stack.
+
+## Goal (as captured in the original plan)
+
+Close out coverage for the `org.apache.juneau.ng.*` packages once `TODO-31` 
(inject-aware microservice) and `TODO-33` (dynamic child REST resources) 
landed. Specifically:
+
+- Build a cross-transport remote-interface test suite against a real Jetty 
pipeline.
+- Bring `org.apache.juneau.ng.http` to **≥95% instruction coverage**.
+- Plug any residual gaps in the five transport modules.
+- Confirm no regressions and that the full `juneau-utest` suite stayed in the 
~33s range.
+
+## Outcome
+
+- **`org.apache.juneau.ng.http`** (`juneau-rest-common`): **95% instructions / 
80% branches** (was 38% / 20%).
+- **`juneau-utest`** grew from 50,181 tests to **50,491 tests**, 0 failures, 0 
errors. Runtime ~33s, unchanged.
+- **All transport modules**: 77–88% instructions / 71–100% branches — 
unchanged here; transport-side branches that need real wire-level fault 
injection are left for a future targeted pass and the residual hostile-server 
edge cases remain out of scope (see _Deferred_ below).
+
+## Phase A — Cross-transport remote-interface tests
+
+Boots a real Jetty pipeline on an ephemeral port and runs a focused scenario 
set through the NG remote-interface proxy against five live transports.
+
+- **`juneau-utest/.../microservice/MicroserviceTestFixture.java`** — JUnit 5 
extension that starts a `JettyMicroservice` on port 0 from one or more 
`@Configuration` classes and exposes `getRootUrl()`. Resolves the actual bound 
port via `ServerConnector.getLocalPort()` and pins the host to `localhost` to 
keep the URL deterministic across dev machines.
+- **`juneau-utest/.../ng/rest/NgRemoteInterfaceTransport_Test.java`** — 
parameterized over the five NG transports (`apache-hc45`, `apache-hc5`, 
`java-http`, `okhttp`, `jetty`). Server side is a single `BasicRestServlet` 
with `defaultAccept = "text/plain"` so assertions can compare plain wire bytes 
without HTML wrapping. Twelve scenarios × five transports = **60 tests**.
+
+Covered scenarios:
+
+- GET with `@Path` (single segment) and `@Query` (single value).
+- POST with `@Content` (string).
+- `@Header` propagation.
+- Response status: 200 with body, 204 no-content, 404 → remote exception 
mapping.
+- Concurrent calls via `Executors.newFixedThreadPool` to confirm transports 
stay thread-safe under contention.
+
+Scenarios that the original plan called for but the **current NG remote 
client** does not yet support (map/bean `@Query`, `@FormData`, bean `@Content`, 
`Reader`/`InputStream` `@Content`, end-to-end `@Remote(rrpc=true)`) were 
intentionally **scoped out**. The classic `RestClient` already covers them 
through `MockRestClient`; the NG proxy is API-incomplete on these surfaces and 
we should drive coverage as those features ship rather than write tests that 
have nothing to bind to.
+
+## Phase B — `org.apache.juneau.ng.http` coverage closeout
+
+A small set of parametric tests cover the bulk of the previously-uncovered 
surface (named response classes, named header classes, body/part 
implementations, and the static-factory facades) without committing to one test 
class per type.
+
+- **`juneau-utest/.../ng/NgPackageScanner.java`** — shared utility that walks 
the classpath (JARs or exploded `target/classes` directories) and enumerates 
the concrete classes in a given package. Used by every parametric test below so 
we don't hand-maintain class lists.
+- **`juneau-utest/.../ng/http/response/NgNamedResponses_Test.java`** — 
parametric over every concrete subclass of `BasicHttpResponse` / 
`BasicHttpException` in `org.apache.juneau.ng.http.response` (**57 tests**). 
Reads each class's `STATUS_CODE` / `REASON_PHRASE` constants, exercises every 
public constructor (including null cause / null message / null body paths), 
verifies `getStatusCode`, `getStatusLine`, `getHeaders`, `toString`, and runs 
`withBody(...)` / `withHeader(...)` mutator cha [...]
+- **`juneau-utest/.../ng/http/header/NgNamedHeaders_Test.java`** — parametric 
over every concrete `HttpHeaderBean` subclass in 
`org.apache.juneau.ng.http.header` (**53 tests**). Walks every public static 
`of(...)`, `ofLazy*(...)` factory, supplying type-appropriate sample values 
dispatched by the header's base type. Calls the typed accessors that match each 
base class on **every instance built** — not just the last — so the 
eager-value, wire-string, and lazy-supplier branches in `getValu [...]
+- **`juneau-utest/.../ng/http/header/PolymorphicHeaders_Test.java`** — 
targeted coverage for the `IfRange` and `RetryAfter` polymorphic value branches 
(**19 tests**). Exercises eager entity-tag, eager date, eager integer, 
wire-string detection (numeric vs entity-tag-with-quote vs HTTP-date), lazy 
suppliers returning each of the typed payloads, lazy suppliers returning null, 
and null-input factory short-circuits.
+- **`juneau-utest/.../ng/http/response/HttpStatusLineBean_Test.java`** — 
covers the `HttpStatusLineBean` factories, custom protocol version, 
null-reason-phrase `toString` path, null-protocol-version rejection, and 
`equals` / `hashCode` (**6 tests**).
+- **`juneau-utest/.../ng/http/entity/HttpBodies_Test.java`** — covers 
`StringBody`, `ByteArrayBody`, `FileBody`, `StreamBody`, `HttpBodyBean`, and 
`MultipartBody`: default vs explicit content type, repeatability flags, 
defensive copy semantics, `writeTo(OutputStream)`, the multipart builder 
pattern, and the part factories (**15 tests**).
+- **`juneau-utest/.../ng/http/part/HttpParts_Test.java`** — smoke tests for 
`HttpPartBean` factories / `equals` / `hashCode` / `toString` and for 
`PartList.getFirst` hit / miss plus the null-value skip path in 
`PartList.writeTo` / `toString` (**~10 tests**).
+- **`juneau-utest/.../ng/http/HttpFactoryFacades_Test.java`** — parametric 
over every public static method on the three façade classes (`HttpHeaders`, 
`HttpBodies`, `HttpResponses`). Each is a thin delegation to an underlying 
factory; reflectively invoking every overload with type-appropriate sample args 
closes out ~300 otherwise-uncovered delegation instructions (**152 tests**).
+
+## Phase C / D — Verify + archive
+
+- `./scripts/coverage.py --run 
juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/ng/http/` — 
**95% / 80%**, ≥95% goal met.
+- Full `juneau-utest` suite: **50,491 tests, 0 failures, 0 errors**, ~33s 
runtime.
+- This file replaces the live plan; `[TODO-11]` removed from `todo/TODO.md`.
+
+## Deferred (intentionally not closed by this pass)
+
+- **`org.apache.juneau.ng.http.remote.RrpcInterfaceMeta`** (~132 missed 
instructions): the RRPC proxy hasn't been wired into the NG remote client yet, 
so there is no useful end-to-end path to test. Pick this up when the NG remote 
client grows `@Remote(rrpc=true)` support.
+- **NG remote-client gaps** (bean-shaped `@Query` / `@FormData`, `Reader` / 
`InputStream` request bodies, multi-segment `@Path`, RRPC): write the 
corresponding `NgRemoteInterfaceTransport_Test` scenarios as those features 
ship.
+- **Hostile-server transport-edge cases** (truncated response body, malformed 
status line, multi-value header carry-through, idle connection release under 
abort): the parametric Jetty fixture from Phase A already exercises the happy 
paths across all five transports. The remaining transport-side branches live 
behind real wire faults and want a `com.sun.net.httpserver.HttpServer` harness 
modelled on the existing `ApacheHc45Transport_Test` rather than the Juneau 
pipeline used here. Track se [...]
diff --git a/todo/TODO-11-restclient-ng-coverage-closeout.md 
b/todo/TODO-11-restclient-ng-coverage-closeout.md
deleted file mode 100644
index 43e74f13b4..0000000000
--- a/todo/TODO-11-restclient-ng-coverage-closeout.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# TODO-11: RestClient NG — Coverage Closeout + Cross-Transport 
Remote-Interface Tests
-
-> **Status:** Implementation, docs, and release notes for the next-generation 
REST client and HTTP stack (`org.apache.juneau.ng.*`) have shipped. The 
original design plan is archived in 
`todo/FINISHED-11a-restclient-ng-design-plan.md`. This file tracks the 
**remaining** cleanup before `TODO-11` can be fully retired.
-
-## Remaining work
-
-### A. Cross-transport remote-interface tests  (blocked on `[TODO-31]`)
-
-Existing `org.apache.juneau.http.remote` tests cover remote-interface behavior 
end-to-end but only against the classic `MockRestClient` — they never go 
through any real wire transport. The new NG transports each have their own 
basic coverage against `com.sun.net.httpserver.HttpServer` with canned 
responses, but nothing exercises a Juneau remote proxy through a real Juneau 
REST pipeline on the server side.
-
-Once `[TODO-31]` lands, build:
-
-- `MicroserviceTestFixture` in `juneau-utest` — JUnit 5 extension that boots a 
`JettyMicroservice` on port 0 from one or more `@Configuration` classes and 
exposes `getRootUrl()`. Tears down in `afterAll`.
-- `NgRemoteInterfaceTransport_Test` in `juneau-utest` — parameterized over the 
five NG transports (`apache-hc45`, `apache-hc5`, `java-http`, `okhttp`, 
`jetty`) via `@ParameterizedTest` + `@MethodSource`. Server side is a single 
`@Rest` resource provided as a `@Bean` from a test `@Configuration`.
-
-Focused scenario set per transport (each scenario runs 5x — once per 
transport):
-
-- GET with `@Path` (single + multi-segment) and `@Query` (single + map + bean).
-- POST with `@Content` (string, bean, `Reader`, `InputStream`).
-- POST with `@FormData` (single, map, bean).
-- Header propagation (`@Header` on parameter, default headers on the client).
-- Response status: 200 with body, 204 no-content, 404 -> remote exception 
mapping.
-- One end-to-end RRPC scenario through `@Remote(rrpc=true)` to confirm 
bidirectional bean marshalling.
-
-5 transports x ~15 scenarios = ~75 executions. This is the primary lift for 
transport-module coverage.
-
-### B. `org.apache.juneau.ng.http` coverage closeout  (independent — can land 
before TODO-31)
-
-The bulk of the uncovered surface is in named response classes (`Ok`, 
`Created`, `NotFound`, ...) and RFC-named header classes (`Accept`, 
`ContentType`, ...). Cover with two parametric tests:
-
-- 
`juneau-utest/src/test/java/org/apache/juneau/ng/http/response/NgNamedResponses_Test.java`
 — reflectively enumerate every public subclass of the response base in 
`org.apache.juneau.ng.http.response`; for each, invoke every public constructor 
/ factory, call all public getters, run `writeTo(OutputStream)`, verify status 
code matches the RFC code.
-- 
`juneau-utest/src/test/java/org/apache/juneau/ng/http/header/NgNamedHeaders_Test.java`
 — same parametric pattern over `org.apache.juneau.ng.http.header.*`. Walks 
`of(...)` factories, value accessors, and `writeTo` / wire-format paths.
-- Fill targeted gaps in `HttpBody`, `HttpHeaders`, `HttpResource` discovered 
by `./scripts/coverage.py --branches` (small one-shot tests).
-
-### C. Transport-module residual gaps  (after A)
-
-After the parameterized suite from A has run, fill what remains via 
`./scripts/coverage.py --branches`:
-
-- `close()` / connection-release hooks on each transport's response type.
-- Header carry-through for multi-value and quoted-value cases.
-- Streaming-body request paths (each transport's `InputStream` consumption 
code).
-
-For hostile-server edge cases (truncated body, malformed status line), use 
`com.sun.net.httpserver.HttpServer` following the existing pattern in 
`ApacheHc45Transport_Test`.
-
-### D. Verify + archive
-
-- `./scripts/coverage.py --run` against 
`juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/ng/http/`; 
confirm >=95% instruction coverage.
-- `./scripts/coverage.py --branches` for every NG package; address remaining 
gaps.
-- `./scripts/test.py` full suite; confirm no regressions and runtime stays in 
the ~33s range.
-- Archive this file as `todo/FINISHED-11b-restclient-ng-coverage-closeout.md` 
and remove `[TODO-11]` from `todo/TODO.md`.
-
-## Latest coverage measurement (May 2026)
-
-Measured via `./scripts/coverage.py` against `juneau-utest/target/jacoco.exec`.
-
-| Package / Module | Branches | Instructions |
-|---|---|---|
-| `org.apache.juneau.ng.http` (in `juneau-rest-common`) | 20% | **38%** |
-| `org.apache.juneau.ng.rest.client` (in `juneau-rest-client`) | 89% | 94% |
-| `org.apache.juneau.ng.rest.mock` (in `juneau-rest-mock`) | 98% | 96% |
-| Apache HC 4.5 transport (`juneau-ng-rest-client-apache-httpclient-45`) | 71% 
| 81% |
-| Apache HC 5 transport (`juneau-ng-rest-client-apache-httpclient-50`) | 79% | 
85% |
-| JDK `HttpClient` transport (`juneau-ng-rest-client-java-httpclient`) | 100% 
| 78% |
-| OkHttp transport (`juneau-ng-rest-client-okhttp`) | 72% | 88% |
-| Jetty transport (`juneau-ng-rest-client-jetty`) | 75% | 77% |
-
-## Out of scope
-
-- Promoting the NG stack from beta to stable.
-- Deprecating or removing the classic `RestClient` / `juneau-rest-common`.
diff --git a/todo/TODO.md b/todo/TODO.md
index bab1163909..5fb2d9a748 100644
--- a/todo/TODO.md
+++ b/todo/TODO.md
@@ -17,8 +17,6 @@
 
 - [TODO-10] Move `org.apache.juneau.http.annotation` from `juneau-marshall` 
into `juneau-rest-common` (already done for the annotation classes — plan 
tracks remaining follow-on cleanup). See 
`todo/TODO-10-move-http-annotation-to-rest-common.md`.
 
-- [TODO-11] RestClient NG closeout: cross-transport remote-interface test 
suite (blocked on [TODO-31]) plus NG http package coverage (independent). 
Implementation has shipped (see 
`todo/FINISHED-11a-restclient-ng-design-plan.md`). Active checklist in 
`todo/TODO-11-restclient-ng-coverage-closeout.md`.
-
 - [TODO-12] Schema validation mode for parsers and serializers: wire `@Schema` 
validation into the bean property get/set lifecycle gated by a new 
`validateSchema` flag on `MarshallingContext`. See 
`todo/TODO-12-schema-validation.md`.
 
 - [TODO-17] Audit 9.2.x changes (juneau-docs release notes 9.2.0 / 9.5.0 + git 
history since 9.1.0) for breaking changes and populate the v9.5 Migration Guide 
at juneau-docs/pages/topics/23.01.V9.5-migration-guide.md with Old→New rows for 
each. Focus on removed APIs, renamed annotations/classes/methods, changed 
default behaviors, and any annotation-attribute semantics changes.


Reply via email to