You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/10/05 22:19:38 UTC

(juneau) branch master updated: Bind non-public records (BeanMeta record accessor access) and harden JSON parser strictness (DEL round-trip, reject hex/octal literals); RemoteClient classifies java.time scalars and parses untyped decimals as Double; fix the MCP elicitation stale-connection flake by draining unconsumed chunked request bodies in RequestContent (keep-alive regression test); fix DataTablesAjax_BrowserTest CI failure; lift rowActions/selection/bulk through CardEnvelope.liftTable, whitelist copyLink, and add j [...]

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 641fadab09 Bind non-public records (BeanMeta record accessor access) and harden JSON parser strictness (DEL round-trip, reject hex/octal literals); RemoteClient classifies java.time scalars and parses untyped decimals as Double; fix the MCP elicitation stale-connection flake by draining unconsumed chunked request bodies in RequestContent (keep-alive regression test); fix DataTablesAjax_BrowserTest CI failure; lift rowActions/selection/bulk through CardEnvelope.liftTable, whitelis [...]
641fadab09 is described below

commit 641fadab09cc44e451b28b58e10842841e522c8f
Author: James Bognar <ja...@apache.org>
AuthorDate: Mon Oct 5 18:19:32 2026 -0400

    Bind non-public records (BeanMeta record accessor access) and harden JSON parser strictness (DEL round-trip, reject hex/octal literals); RemoteClient classifies java.time scalars and parses untyped decimals as Double; fix the MCP elicitation stale-connection flake by draining unconsumed chunked request bodies in RequestContent (keep-alive regression test); fix DataTablesAjax_BrowserTest CI failure; lift rowActions/selection/bulk through CardEnvelope.liftTable, whitelist copyLink, and  [...]
---
 .../org/apache/juneau/commons/bean/BeanMeta.java   |  33 ++++-
 .../commons/bean/BeanMeta_RecordAccess_Test.java   |  84 +++++++++++
 .../juneau/commons/utils/StringUtils_Test.java     |   5 +
 .../main/java/org/apache/juneau/config/Config.java |   2 +-
 .../apache/juneau/marshall/parquet/Parquet.java    |   4 +-
 .../juneau/marshall/parquet/ParquetConfig.java     |   8 +-
 .../juneau/marshall/parser/ParserReader.java       |  48 +++++--
 .../juneau/marshall/yaml/YamlSerializer.java       |  18 +--
 .../org/apache/juneau/NonPublicRecord_Test.java    | 103 ++++++++++++++
 .../java/org/apache/juneau/ParserReader_Test.java  |  64 +++++++++
 .../juneau/marshall/json/JsonParser_Test.java      |  25 ++++
 .../v20260728/McpElicitationIntegration_Test.java  |  16 +--
 .../mcp/v20260728/McpMrtrIntegration_Test.java     |  17 +--
 ...nt_ChunkedKeepAlive_JettyMicroservice_Test.java | 157 +++++++++++++++++++++
 .../juneau/rest/client/HttpTransportBuilder.java   |   6 +-
 .../org/apache/juneau/rest/client/RestRequest.java |   8 +-
 .../juneau/rest/client/TransportRequest.java       |   4 +-
 .../juneau/rest/client/remote/RemoteClient.java    |   9 +-
 .../juneau/rest/client/RemoteClient_Test.java      |  26 ++++
 ...moteClient_InvocationHandlerInternals_Test.java |  18 +++
 .../view/freemarker/console/CardEnvelope.java      |  26 +++-
 .../freemarker/console/ToolkitPackRegistry.java    |   6 +-
 .../view/freemarker/console/CardEnvelope_Test.java |  33 +++++
 .../console/ToolkitPackRegistry_Test.java          |   5 +-
 .../rest/server/console/ConsoleChromeMixin.java    |   2 +-
 .../apache/juneau/rest/server/views/Render.java    |   9 +-
 .../juneau/rest/server/views/ViewsMixin.java       |   4 +-
 .../server/views/DataTablesAjax_BrowserTest.java   |  17 +++
 .../rest/server/views/ViewsMixin_Serving_Test.java |   5 +
 .../src/test/js/datatables-ajax.cjs                |  21 ++-
 .../apache/juneau/rest/server/arg/RestOpArg.java   |   5 +-
 .../rest/server/httppart/RequestContent.java       |  28 ++++
 32 files changed, 730 insertions(+), 86 deletions(-)

diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMeta.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMeta.java
index 0396fab40b..21db4d8260 100644
--- a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMeta.java
+++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanMeta.java
@@ -1205,7 +1205,7 @@ public class BeanMeta<T> {
 			// among the declared constructors; the bean-class visibility check already governs the record itself.
 			var rcon = ci.getDeclaredConstructor(x -> x.hasParameterTypes(paramTypes)).orElse(null);
 			if (rcon != null)
-				return new BeanConstructor(o(rcon.accessible()), components.stream().map(RecordComponent::getName).toList());
+				return new BeanConstructor(o(makeRecordConstructorAccessible(ci, rcon)), components.stream().map(RecordComponent::getName).toList());
 		}
 
 		if (implClassConstructor != null)
@@ -1428,6 +1428,37 @@ public class BeanMeta<T> {
 		return l;
 	}
 
+	/**
+	 * Makes a record's canonical constructor accessible, failing with a clear message if the module system refuses.
+	 *
+	 * @param ci The record class.
+	 * @param rcon The canonical constructor.
+	 * @return The same constructor.
+	 * @throws BeanRuntimeException If the record is non-public and its package is not opened to Juneau.
+	 */
+	static ConstructorInfo makeRecordConstructorAccessible(ClassInfo ci, ConstructorInfo rcon) {
+		return makeRecordConstructorAccessible(ci, rcon, ConstructorInfo::setAccessible);
+	}
+
+	// Package-private seam so tests can force setAccessible() to report failure.
+	static ConstructorInfo makeRecordConstructorAccessible(ClassInfo ci, ConstructorInfo rcon, Predicate<ConstructorInfo> accessor) {
+		// Always attempt setAccessible (a public record nested in a non-public class still needs it);
+		// only fail loudly when the record is not effectively public.
+		var ok = accessor.test(rcon);
+		if (! ok && ! (rcon.isPublic() && isEffectivelyPublic(ci))) {
+			var pkg = ci.getPackage() == null ? "" : ci.getPackage().getName();
+			throw brex(ci, "Could not make the canonical constructor of non-public record '%s' accessible. The package '%s' must be opened to Juneau, e.g. 'opens %s to org.apache.juneau.commons;' in module-info or '--add-opens <module>/%s=ALL-UNNAMED'.", ci.getNameFull(), pkg, pkg, pkg);
+		}
+		return rcon;
+	}
+
+	private static boolean isEffectivelyPublic(ClassInfo ci) {
+		for (Class<?> c = ci.inner(); c != null; c = c.getEnclosingClass())
+			if (! Modifier.isPublic(c.getModifiers()))
+				return false;
+		return true;
+	}
+
 	private static boolean isRecordAccessor(MethodInfo m, ClassInfo ci) {
 		return ci.getRecordComponents().stream()
 			.anyMatch(rc -> rc.getName().equals(m.getNameSimple())
diff --git a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/bean/BeanMeta_RecordAccess_Test.java b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/bean/BeanMeta_RecordAccess_Test.java
new file mode 100644
index 0000000000..65683d2b92
--- /dev/null
+++ b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/bean/BeanMeta_RecordAccess_Test.java
@@ -0,0 +1,84 @@
+/*
+ * 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.commons.bean;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.commons.*;
+import org.apache.juneau.commons.reflect.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests the module-opens diagnostic raised when a non-public record's canonical constructor can't be made accessible.
+ */
+public class BeanMeta_RecordAccess_Test extends TestBase {
+
+	record PackageRec(String name) {}
+	public record PublicRec(String name) {}
+
+	static class Hidden {
+		public record NestedPublicRec(String name) {}
+	}
+
+	private static ConstructorInfo ctor(Class<?> c) {
+		return ClassInfo.of(c).getDeclaredConstructors().get(0);
+	}
+
+	@Test void a01_nonPublicRecord_notOpened_throwsClearMessage() {
+		var ci = ClassInfo.of(PackageRec.class);
+		var e = assertThrows(BeanRuntimeException.class, () -> BeanMeta.makeRecordConstructorAccessible(ci, ctor(PackageRec.class), x -> false));
+		var m = e.getMessage();
+		assertTrue(m.contains("PackageRec"), m);
+		assertTrue(m.contains("opens org.apache.juneau.commons.bean to"), m);
+		assertTrue(m.contains("--add-opens"), m);
+	}
+
+	@Test void a02_nonPublicRecord_opened_returnsConstructor() {
+		var ci = ClassInfo.of(PackageRec.class);
+		var c = ctor(PackageRec.class);
+		assertSame(c, BeanMeta.makeRecordConstructorAccessible(ci, c, x -> true));
+	}
+
+	@Test void a03_publicRecord_failedAccess_doesNotThrow() {
+		var ci = ClassInfo.of(PublicRec.class);
+		var c = ctor(PublicRec.class);
+		assertSame(c, BeanMeta.makeRecordConstructorAccessible(ci, c, x -> false));
+	}
+
+	@Test void a03b_publicRecord_alwaysAttemptsSetAccessible() {
+		var calls = new java.util.concurrent.atomic.AtomicInteger();
+		var c = ctor(PublicRec.class);
+		BeanMeta.makeRecordConstructorAccessible(ClassInfo.of(PublicRec.class), c, x -> { calls.incrementAndGet(); return true; });
+		assertEquals(1, calls.get());
+	}
+
+	@Test void a05_publicRecordInNonPublicClass_failedAccess_throws() {
+		// Not effectively public, so a failed setAccessible is reported.
+		var ci = ClassInfo.of(Hidden.NestedPublicRec.class);
+		assertThrows(BeanRuntimeException.class, () -> BeanMeta.makeRecordConstructorAccessible(ci, ctor(Hidden.NestedPublicRec.class), x -> false));
+	}
+
+	@Test void a06_publicRecordInNonPublicClass_bindsNormally() {
+		var ci = ClassInfo.of(Hidden.NestedPublicRec.class);
+		assertNotNull(BeanMeta.makeRecordConstructorAccessible(ci, ctor(Hidden.NestedPublicRec.class)));
+	}
+
+	@Test void a04_realSetAccessible_nonPublicRecord_succeedsOnClasspath() {
+		var ci = ClassInfo.of(PackageRec.class);
+		assertNotNull(BeanMeta.makeRecordConstructorAccessible(ci, ctor(PackageRec.class)));
+	}
+}
diff --git a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/StringUtils_Test.java b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/StringUtils_Test.java
index 1a785b3c25..5e4944dbfe 100755
--- a/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/StringUtils_Test.java
+++ b/juneau-core/juneau-commons/src/test/java/org/apache/juneau/commons/utils/StringUtils_Test.java
@@ -4698,6 +4698,11 @@ class StringUtils_Test extends TestBase {
 		assertEquals(123.45f, parseNumber("123.45", Float.class));
 		assertEquals(123.45f, parseNumber("123.45", Float.TYPE));
 
+		// Untyped (auto-detect) decimals are Double, never Float (WORK-J0588 regression guard).
+		assertEquals(Double.class, parseNumber("1.5", null).getClass());
+		assertEquals(Double.class, parseNumber("3.4028235E38", Number.class).getClass());
+		assertEquals(Double.class, parseNumber("0.1", Number.class).getClass());
+
 		// Test Long type - triggers code path
 		assertEquals(123L, parseNumber("123", Long.class));
 		assertEquals(123L, parseNumber("123", Long.TYPE));
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
index 166f0374e0..81d0375b7e 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
@@ -1248,7 +1248,7 @@ public class Config extends Context implements ConfigEventListener {
 	}
 
 	/**
-	 * Saves this config file to the specified writer as an INI file.
+	 * Saves this config file to the specified writer using this config's {@link ConfigFormat} (INI, YAML, etc.).
 	 *
 	 * @param w
 	 * 	The writer to send the output to.
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parquet/Parquet.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parquet/Parquet.java
index 5794806bdb..70179cba09 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parquet/Parquet.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parquet/Parquet.java
@@ -33,9 +33,9 @@ import java.lang.annotation.*;
 @Inherited
 public @interface Parquet {
 
-	/** Override Parquet physical type. */
+	/** Override Parquet physical type.  <b>Reserved:</b> currently read by neither the serializer nor the parser, so it has no effect. */
 	String parquetType() default "";
 
-	/** Override logical type. */
+	/** Override logical type.  <b>Reserved:</b> currently read by neither the serializer nor the parser, so it has no effect. */
 	String logicalType() default "";
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parquet/ParquetConfig.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parquet/ParquetConfig.java
index ce600a1028..20e5e5c7f0 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parquet/ParquetConfig.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parquet/ParquetConfig.java
@@ -37,7 +37,13 @@ import org.apache.juneau.marshall.*;
 @ContextApply({ ParquetConfigAnnotation.SerializerApply.class, ParquetConfigAnnotation.ParserApply.class })
 public @interface ParquetConfig {
 
-	/** Compression codec (UNCOMPRESSED, GZIP). */
+	/**
+	 * Compression codec used when <b>writing</b> (<js>"UNCOMPRESSED"</js> or <js>"GZIP"</js>, case-insensitive).
+	 *
+	 * <p>
+	 * Any other value (including <js>"SNAPPY"</js>, which the parser can decode but the serializer cannot write)
+	 * falls back to <js>"UNCOMPRESSED"</js>.  The parser detects the codec from the file and ignores this setting.
+	 */
 	String compressionCodec() default "";
 
 	/** Row group size in bytes. */
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserReader.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserReader.java
index df0421c311..d3eee4340d 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserReader.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/parser/ParserReader.java
@@ -20,6 +20,7 @@ import static org.apache.juneau.commons.utils.Shorts.*;
 import static org.apache.juneau.commons.utils.StringUtils.*;
 
 import java.io.*;
+import java.util.*;
 
 import org.apache.juneau.commons.io.*;
 
@@ -71,7 +72,7 @@ public class ParserReader extends Reader implements Positionable {
 	private int iMark = -1;    // Mark position in buffer
 	private int iEnd;          // The last good character position in the buffer
 	private boolean endReached;
-	private boolean holesExist;
+	private BitSet holes;      // Deleted positions (relative to iMark); null unless an escape was collapsed in the current mark
 	private final boolean unbuffered;
 
 	/**
@@ -131,9 +132,7 @@ public class ParserReader extends Reader implements Positionable {
 	 * @return This object.
 	 */
 	public final ParserReader delete(int count) {
-		for (var i = 0; i < count; i++)
-			buff[iCurrent - i - 1] = 127;
-		holesExist = true;
+		markHoles(iCurrent - count, iCurrent);
 		return this;
 	}
 
@@ -160,16 +159,16 @@ public class ParserReader extends Reader implements Positionable {
 	public final String getMarked(int offsetStart, int offsetEnd) {
 		int offset = 0;
 
-		// Holes are \u00FF 'delete' characters that we need to get rid of now.
-		if (holesExist) {
+		// Holes are positions removed by delete()/replace() that we need to get rid of now.
+		// They are tracked by position (not by sentinel character) so real DEL characters are preserved.
+		if (holes != null) {
 			for (var i = iMark; i < iCurrent; i++) {
-				char c = buff[i];
-				if (c == 127)
+				if (holes.get(i - iMark))
 					offset++;
 				else
-					buff[i - offset] = c;
+					buff[i - offset] = buff[i];
 			}
-			holesExist = false;
+			holes = null;
 		}
 		int start = iMark + offsetStart;
 		int len = iCurrent - iMark + offsetEnd - offsetStart - offset;
@@ -186,6 +185,7 @@ public class ParserReader extends Reader implements Positionable {
 	 */
 	public final void mark() {
 		iMark = iCurrent;
+		holes = null;
 	}
 
 	/**
@@ -358,22 +358,42 @@ public class ParserReader extends Reader implements Positionable {
 		if (c < 0x10000) {
 			if (offset < 1)
 				throw ioex("Buffer underflow.");
+			clearHoles(iCurrent - offset, iCurrent - offset + 1);
 			buff[iCurrent - offset] = (char)c;
 		} else {
 			if (offset < 2)
 				throw ioex("Buffer underflow.");
 			c -= 0x10000;
+			clearHoles(iCurrent - offset, iCurrent - offset + 2);
 			buff[iCurrent - offset] = (char)(0xd800 + (c >> 10));
 			buff[iCurrent - offset + 1] = (char)(0xdc00 + (c & 0x3ff));
 			offset--;
 		}
-		// Fill in the gap with DEL characters.
-		for (var i = 1; i < offset; i++)
-			buff[iCurrent - i] = 127;
-		holesExist |= (offset > 1);
+		// Mark the gap as deleted.
+		if (offset > 1)
+			markHoles(iCurrent - offset + 1, iCurrent);
 		return this;
 	}
 
+	/*
+	 * Un-deletes [from,to) because a replacement character is being written there.
+	 */
+	private void clearHoles(int from, int to) {
+		if (holes != null && iMark >= 0)
+			holes.clear(Math.max(from - iMark, 0), Math.max(to - iMark, 0));
+	}
+
+	/*
+	 * Records [from,to) as deleted; positions are stored relative to iMark so they survive buffer shifts.
+	 */
+	private void markHoles(int from, int to) {
+		if (iMark < 0)
+			return;
+		if (holes == null)
+			holes = new BitSet();
+		holes.set(Math.max(from - iMark, 0), Math.max(to - iMark, 0));
+	}
+
 	/**
 	 * Pushes the last read character back into the stream.
 	 *
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlSerializer.java
index fc35914474..bcac90f965 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlSerializer.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/yaml/YamlSerializer.java
@@ -85,27 +85,27 @@ import org.apache.juneau.marshall.stream.*;
  * 	<jv>serializer</jv> = YamlSerializer.<jsf>DEFAULT</jsf>.copy().ws().build();
  *
  * 	<jc>// Serialize a POJO to YAML</jc>
- * 	String <jv>yaml</jv> = <jv>serializer</jv>.write(<jv>someObject</jv>);
+ * 	<jv>yaml</jv> = <jv>serializer</jv>.write(<jv>someObject</jv>);
  * </p>
  *
- * <h5 class='figure'>Example output (Map of name/age):</h5>
+ * <h5 class='figure'>Example output (LinkedHashMap of name/age):</h5>
  * <p class='bcode'>
  * 	name: Alice
  * 	age: 30
  * </p>
  *
- * <h5 class='figure'>Complex (nested object + array):</h5>
+ * <h5 class='figure'>Complex (nested bean + array; bean properties are sorted alphabetically by default):</h5>
  * <p class='bcode'>
- * 	name: Alice
- * 	age: 30
  * 	address:
- * 	  street: 123 Main St
  * 	  city: Boston
  * 	  state: MA
+ * 	  street: "123 Main St"
+ * 	age: 30
+ * 	name: Alice
  * 	tags:
- * 	- a
- * 	- b
- * 	- c
+ * 	  - a
+ * 	  - b
+ * 	  - c
  * </p>
  *
  * <h5 class='section'>Limitations compared to JSON</h5>
diff --git a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/NonPublicRecord_Test.java b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/NonPublicRecord_Test.java
new file mode 100644
index 0000000000..1a850fcd2a
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/NonPublicRecord_Test.java
@@ -0,0 +1,103 @@
+/*
+ * 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;
+
+import static org.apache.juneau.commons.reflect.Visibility.*;
+import static org.apache.juneau.test.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.marshall.json5.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests binding of non-public (private / package-private) records.
+ */
+class NonPublicRecord_Test extends TestBase {
+
+	public record PublicRec(String name, int age) {}
+	record PackageRec(String name, int age) {}
+	private record PrivateRec(String name, int age) {}
+	protected record ProtectedRec(String name, int age) {}
+
+	static class Hidden {
+		public record NestedPublicRec(String name, int age) {}
+	}
+
+	private static Json5Serializer.Builder sb() {
+		return Json5Serializer.create();
+	}
+
+	private static Json5Parser.Builder pb() {
+		return Json5Parser.create();
+	}
+
+	@Test void a01_publicRecord_default() throws Exception {
+		assertString("{age:1,name:'a'}", Json5Serializer.DEFAULT.write(new PublicRec("a", 1)));
+		assertBean(Json5Parser.DEFAULT.read("{name:'a',age:1}", PublicRec.class), "name,age", "a,1");
+	}
+
+	@Test void a02_privateRecord_default_notBound() throws Exception {
+		var s = Json5Serializer.DEFAULT.write(new PrivateRec("a", 1));
+		assertFalse(s.startsWith("{"), s);
+	}
+
+	@Test void a03_packageRecord_default_notBound() throws Exception {
+		var s = Json5Serializer.DEFAULT.write(new PackageRec("a", 1));
+		assertFalse(s.startsWith("{"), s);
+	}
+
+	@Test void b01_privateRecord_roundTrip() throws Exception {
+		var s = sb().beanClassVisibility(PRIVATE).beanConstructorVisibility(PRIVATE).build();
+		var p = pb().beanClassVisibility(PRIVATE).beanConstructorVisibility(PRIVATE).build();
+		var json = s.write(new PrivateRec("a", 1));
+		assertString("{age:1,name:'a'}", json);
+		assertBean(p.read(json, PrivateRec.class), "name,age", "a,1");
+	}
+
+	@Test void b02_packageRecord_roundTrip() throws Exception {
+		var s = sb().beanClassVisibility(PRIVATE).beanConstructorVisibility(PRIVATE).build();
+		var p = pb().beanClassVisibility(PRIVATE).beanConstructorVisibility(PRIVATE).build();
+		var json = s.write(new PackageRec("a", 1));
+		assertString("{age:1,name:'a'}", json);
+		assertBean(p.read(json, PackageRec.class), "name,age", "a,1");
+	}
+
+	@Test void b03_protectedRecord_roundTrip() throws Exception {
+		var s = sb().beanClassVisibility(PROTECTED).beanConstructorVisibility(PROTECTED).build();
+		var p = pb().beanClassVisibility(PROTECTED).beanConstructorVisibility(PROTECTED).build();
+		var json = s.write(new ProtectedRec("a", 1));
+		assertString("{age:1,name:'a'}", json);
+		assertBean(p.read(json, ProtectedRec.class), "name,age", "a,1");
+	}
+
+	@Test void b04_privateRecord_classVisibilityOnly() throws Exception {
+		// Class visibility alone is enough; a record's canonical constructor is implied by the record declaration.
+		var s = sb().beanClassVisibility(PRIVATE).build();
+		var p = pb().beanClassVisibility(PRIVATE).build();
+		var json = s.write(new PrivateRec("a", 1));
+		assertString("{age:1,name:'a'}", json);
+		assertBean(p.read(json, PrivateRec.class), "name,age", "a,1");
+	}
+
+	@Test void b05_publicRecordInPackagePrivateClass_roundTrip() throws Exception {
+		var s = sb().beanClassVisibility(PRIVATE).build();
+		var p = pb().beanClassVisibility(PRIVATE).build();
+		var json = s.write(new Hidden.NestedPublicRec("a", 1));
+		assertString("{age:1,name:'a'}", json);
+		assertBean(p.read(json, Hidden.NestedPublicRec.class), "name,age", "a,1");
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParserReader_Test.java b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParserReader_Test.java
index be196d9440..71b21d1132 100755
--- a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParserReader_Test.java
+++ b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/ParserReader_Test.java
@@ -20,7 +20,12 @@ import static org.junit.jupiter.api.Assertions.*;
 
 import java.io.*;
 
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.json5.*;
+import org.apache.juneau.marshall.json5l.*;
+import org.apache.juneau.marshall.jsonl.*;
 import org.apache.juneau.marshall.parser.*;
+import org.apache.juneau.marshall.uon.*;
 import org.junit.jupiter.api.*;
 
 @SuppressWarnings({
@@ -160,6 +165,65 @@ class ParserReader_Test extends TestBase {
 	// Utility methods
 	//====================================================================================================
 
+	//====================================================================================================
+	// Holes created by delete()/replace() are removed by position; real DEL (0x7F) characters survive.
+	//====================================================================================================
+	@Test void a03_holesPreserveRealDelCharacters() throws Exception {
+		var del = "\u007F";
+
+		// replace() with offset > 1 collapses an escape sequence; surrounding DELs are preserved.
+		var pr = createParserReader("\"a" + del + "\\u0041" + del + "b\"");
+		pr.mark();
+		read(pr, 4);                  // "a<DEL>\
+		pr.delete();                  // drop the backslash
+		read(pr, 5);                  // u0041
+		pr.replace('A', 6);           // collapse \u0041 (offset covers 6 chars read so far)
+		read(pr, 1);                  // <DEL>
+		read(pr, 2);                  // b"
+		assertEquals("a" + del + "A" + del + "b", pr.getMarked(1, -1));
+
+		// delete(count) with several holes plus real DELs.
+		pr = createParserReader("x" + del + "~~" + del + "y~" + del);
+		pr.mark();
+		read(pr, 4);
+		pr.delete(2);
+		read(pr, 3);
+		pr.delete(1);
+		read(pr, 1);
+		assertEquals("x" + del + del + "y" + del, pr.getMarked());
+
+		// Holes do not leak into a later mark.
+		pr = createParserReader("~a" + del + "b");
+		pr.mark();
+		read(pr, 1);
+		pr.delete();
+		assertEquals("", pr.getMarked());
+		pr.mark();
+		read(pr, 3);
+		assertEquals("a" + del + "b", pr.getMarked());
+	}
+
+
+	//====================================================================================================
+	// Parsers that collapse escapes through ParserReader.delete()/replace() keep real DEL (U+007F) characters.
+	// (YAML, TOML, Prototext and INI do not use delete()/replace(), so they are not exposed to this.)
+	//====================================================================================================
+	@Test void a04_delSurvivesEscapesInParsers() throws Exception {
+		var del = "\u007F";
+		var expected = "a" + del + "b\nc" + del;
+
+		assertEquals(expected, JsonParser.DEFAULT.read("\"a" + del + "b\\nc\\u007f\"", String.class));
+		assertEquals(expected, Json5Parser.DEFAULT.read("\"a" + del + "b\\nc\\u007f\"", String.class));
+		assertEquals(expected, Json5Parser.DEFAULT.read("'a" + del + "b\\nc" + del + "'", String.class));
+		assertEquals(expected, JsonParser.DEFAULT.read("\"a" + del + "b\\nc\\u007f\"", Object.class));
+		assertEquals("a" + del + "b'c" + del, UonParser.DEFAULT.read("'a" + del + "b~'c" + del + "'", String.class));
+
+		var jl = JsonlParser.DEFAULT.read("\"a" + del + "b\\nc\\u007f\"", String.class);
+		assertEquals(expected, jl);
+		var j5l = Json5lParser.DEFAULT.read("'a" + del + "b\\nc" + del + "'", String.class);
+		assertEquals(expected, j5l);
+	}
+
 	private static String read(ParserReader r) throws IOException {
 		return read(r, Integer.MAX_VALUE);
 	}
diff --git a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json/JsonParser_Test.java b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json/JsonParser_Test.java
index 7811c941b2..297951998e 100755
--- a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json/JsonParser_Test.java
+++ b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/json/JsonParser_Test.java
@@ -110,6 +110,8 @@ class JsonParser_Test extends TestBase {
 		assertThrowsWithMessage(Exception.class, "Invalid JSON number", ()->p.read("-0123", Number.class));
 		assertThrowsWithMessage(Exception.class, "Invalid JSON number", ()->p.read("0x123", Number.class));
 		assertThrowsWithMessage(Exception.class, "Invalid JSON number", ()->p.read("-0x123", Number.class));
+		assertThrows(ParseException.class, ()->p.read("017", Number.class));
+		assertThrows(ParseException.class, ()->p.read("-017", Number.class));
 	}
 
 	@Test void a06b_validJsonNumbers() throws Exception {
@@ -117,6 +119,29 @@ class JsonParser_Test extends TestBase {
 		assertEquals(0, p.read("-0", Number.class).intValue());
 	}
 
+	@Test void a06c_laxJson5AcceptsHexAndOctal() throws Exception {
+		var p5 = Json5Parser.DEFAULT;
+		assertEquals(31, p5.read("0x1F", Number.class).intValue());
+		assertEquals(15, p5.read("017", Number.class).intValue());
+	}
+
+	//====================================================================================================
+	// DEL (U+007F) is legal unescaped in JSON strings and must survive a round-trip.
+	//====================================================================================================
+	@Test void a06d_delCharacterRoundTrip() throws Exception {
+		var s = "a\u007Fb";
+		var json = JsonSerializer.DEFAULT.toString(s);
+		assertString(s, p.read(json, String.class));
+		assertEquals(3, p.read(json, String.class).length());
+	}
+
+	@Test void a06e_delCharacterUnescaped() throws Exception {
+		var r = p.read("\"a\u007Fb\"", String.class);
+		assertString("a\u007Fb", r);
+		assertEquals(3, r.length());
+		assertEquals(0x7F, r.charAt(1));
+	}
+
 	//====================================================================================================
 	// testUnquotedStrings
 	// Lax parser allows unquoted strings if POJO can be converted from a string.
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpElicitationIntegration_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpElicitationIntegration_Test.java
index 7782b6e920..ca4c66846c 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpElicitationIntegration_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpElicitationIntegration_Test.java
@@ -28,7 +28,6 @@ import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.marshall.json.*;
 import org.apache.juneau.marshall.marshaller.Json;
 import org.apache.juneau.microservice.*;
-import org.apache.juneau.rest.client.RestCallInterceptor;
 import org.apache.juneau.rest.server.*;
 import org.apache.juneau.rest.server.mcp.*;
 import org.apache.juneau.rest.server.mcp.v20260728.*;
@@ -124,26 +123,13 @@ class McpElicitationIntegration_Test extends TestBase {
 	static MicroserviceTestFixture fixture = MicroserviceTestFixture.create()
 		.configurations(FixtureConfig.class);
 
-	// Forces a fresh TCP connection per request instead of reusing a pooled keep-alive one: a client that
-	// issues two or more requests (pause then resume) can otherwise race the embedded Jetty fixture
-	// tearing down an idle pooled connection between calls. That surfaces as NoHttpResponseException /
-	// ApacheHc45Transport$StaleConnectionException on the elicitation resume POST. Resume is a mutating
-	// tools/call (requestState + ElicitAction, including cancel) and must not be replayed, so this test
-	// client disables keep-alive rather than retrying. Same interceptor as McpMrtrIntegration_Test.
-	private static final RestCallInterceptor CLOSE_CONNECTION_PER_REQUEST = new RestCallInterceptor() {
-		@Override public void onInit(org.apache.juneau.rest.client.RestRequest req) {
-			req.header("Connection", "close");
-		}
-	};
-
 	private static McpClient.Builder clientBuilder(boolean withElicitation) {
 		var caps = new ClientCapabilities();
 		if (withElicitation)
 			caps.setElicitation(new ElicitationCapability());
 		return McpClient.builder()
 			.endpoint(fixture.getRootUrl() + "/")
-			.clientCapabilities(caps)
-			.interceptor(CLOSE_CONNECTION_PER_REQUEST);
+			.clientCapabilities(caps);
 	}
 
 	/**
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
index 97768ebcd5..95f16cda48 100644
--- a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpMrtrIntegration_Test.java
@@ -29,7 +29,6 @@ import org.apache.juneau.commons.inject.*;
 import org.apache.juneau.marshall.json.*;
 import org.apache.juneau.marshall.marshaller.Json;
 import org.apache.juneau.microservice.*;
-import org.apache.juneau.rest.client.RestCallInterceptor;
 import org.apache.juneau.rest.server.*;
 import org.apache.juneau.rest.server.mcp.*;
 import org.apache.juneau.rest.server.mcp.v20260728.*;
@@ -184,24 +183,13 @@ class McpMrtrIntegration_Test extends TestBase {
 	static MicroserviceTestFixture fixture = MicroserviceTestFixture.create()
 		.configurations(FixtureConfig.class);
 
-	// Forces a fresh TCP connection per request instead of reusing a pooled keep-alive one: a client that
-	// issues two or more requests (every test below does) can otherwise race the embedded Jetty fixture
-	// tearing down/resetting an idle pooled connection between calls, surfacing as a spurious
-	// NoHttpResponseException instead of the response the test actually expects.
-	private static final RestCallInterceptor CLOSE_CONNECTION_PER_REQUEST = new RestCallInterceptor() {
-		@Override public void onInit(org.apache.juneau.rest.client.RestRequest req) {
-			req.header("Connection", "close");
-		}
-	};
-
 	private static McpClient.Builder clientBuilder(boolean withElicitation) {
 		var caps = new ClientCapabilities();
 		if (withElicitation)
 			caps.setElicitation(new ElicitationCapability());
 		return McpClient.builder()
 			.endpoint(fixture.getRootUrl() + "/")
-			.clientCapabilities(caps)
-			.interceptor(CLOSE_CONNECTION_PER_REQUEST);
+			.clientCapabilities(caps);
 	}
 
 	private static McpClient.Builder clientBuilderAt(String path, boolean withElicitation) {
@@ -210,8 +198,7 @@ class McpMrtrIntegration_Test extends TestBase {
 			caps.setElicitation(new ElicitationCapability());
 		return McpClient.builder()
 			.endpoint(fixture.getRootUrl() + path)
-			.clientCapabilities(caps)
-			.interceptor(CLOSE_CONNECTION_PER_REQUEST);
+			.clientCapabilities(caps);
 	}
 
 	// =================================================================================================================
diff --git a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestContent_ChunkedKeepAlive_JettyMicroservice_Test.java b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestContent_ChunkedKeepAlive_JettyMicroservice_Test.java
new file mode 100644
index 0000000000..42347c97fd
--- /dev/null
+++ b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/server/httppart/RequestContent_ChunkedKeepAlive_JettyMicroservice_Test.java
@@ -0,0 +1,157 @@
+/*
+ * 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.rest.server.httppart;
+
+import static java.nio.charset.StandardCharsets.*;
+import static org.apache.juneau.test.bct.BctAssertions.*;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.http.Content;
+import org.apache.juneau.marshall.collections.*;
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.servlet.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.extension.*;
+
+import jakarta.servlet.*;
+
+/**
+ * Real-Jetty regression coverage for {@link RequestContent} draining a parsed request entity to end-of-stream.
+ *
+ * <p>
+ * A reader parser stops as soon as the top-level value is complete, so without an explicit drain the
+ * zero-length terminating chunk of a {@code Transfer-Encoding: chunked} request body is never read whenever it
+ * arrives after the JSON bytes.  The container then finds unconsumed request content when the (already committed)
+ * response completes and aborts the connection &mdash; with no {@code Connection: close} header, because the
+ * headers have already gone out &mdash; so a client reusing that pooled keep-alive connection for its next request
+ * fails with an immediate EOF.  Streaming HTTP clients (the JDK {@code HttpClient} with an
+ * {@code ofInputStream} publisher, Apache HttpClient with a streaming entity) routinely flush the terminator
+ * separately from the content, so this surfaced as an intermittent stale-connection failure on the MCP
+ * elicitation resume POST.
+ *
+ * <p>
+ * The exchanges below are driven over a raw socket so the terminator timing is deterministic rather than at the
+ * mercy of a client library's write scheduling.
+ */
+@org.apache.juneau.testing.JettyMicroserviceTest
+@SuppressWarnings({
+	"java:S2925" // The sleep deliberately holds back the chunk terminator past the handler's completion; there is no server-side seam to latch on.
+})
+class RequestContent_ChunkedKeepAlive_JettyMicroservice_Test extends TestBase {
+
+	@Rest(paths="/echo/*", serializers=JsonSerializer.class, parsers=JsonParser.class, defaultAccept="application/json")
+	public static class EchoServlet extends RestServlet {
+		private static final long serialVersionUID = 1L;
+
+		@RestPost("/")
+		public JsonMap echo(@Content JsonMap body) {
+			return body;
+		}
+	}
+
+	@Configuration
+	public static class Config {
+		@Bean(name="echoServlet")
+		public Servlet echoServlet() { return new EchoServlet(); }
+	}
+
+	@RegisterExtension
+	static MicroserviceTestFixture fixture = MicroserviceTestFixture.create()
+		.configurations(Config.class);
+
+	/** How long the terminating chunk is held back: comfortably longer than the handler takes on a warm connection. */
+	private static final long TERMINATOR_DELAY_MS = 300;
+
+	@Test void a01_lateChunkTerminator_keepsConnectionAlive() throws Exception {
+		var uri = fixture.getRootUrl();
+		try (var socket = new Socket(uri.getHost(), uri.getPort())) {
+			socket.setSoTimeout(10_000);
+			var out = socket.getOutputStream();
+			var in = new BufferedInputStream(socket.getInputStream());
+			var statuses = new ArrayList<String>();
+
+			// Warm-up exchange, so the late-terminator exchange below is handled faster than the delay.
+			statuses.add(exchange(out, in, "{\"n\":1}", 0));
+			// The JSON is complete but its 0-chunk arrives only after the handler has returned.
+			statuses.add(exchange(out, in, "{\"n\":2}", TERMINATOR_DELAY_MS));
+			// Reuses the same keep-alive connection; fails with EOF/broken pipe if the server aborted it.
+			statuses.add(exchange(out, in, "{\"n\":3}", 0));
+
+			assertList(statuses, "HTTP/1.1 200 OK {\"n\":1}", "HTTP/1.1 200 OK {\"n\":2}", "HTTP/1.1 200 OK {\"n\":3}");
+		}
+	}
+
+	// Sends one chunked POST (terminator delayed by delayMs) and returns "<status-line> <body>" of the response.
+	private static String exchange(OutputStream out, InputStream in, String json, long delayMs) throws Exception {
+		var body = json.getBytes(UTF_8);
+		out.write(("POST /echo/ HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nAccept: application/json\r\n"
+			+ "Transfer-Encoding: chunked\r\n\r\n" + Integer.toHexString(body.length) + "\r\n").getBytes(ISO_8859_1));
+		out.write(body);
+		out.write("\r\n".getBytes(ISO_8859_1));
+		out.flush();
+		if (delayMs > 0)
+			Thread.sleep(delayMs);
+		out.write("0\r\n\r\n".getBytes(ISO_8859_1));
+		out.flush();
+		return readResponse(in);
+	}
+
+	// Reads one HTTP/1.1 response (chunked or Content-Length framed) and returns "<status-line> <body>".
+	private static String readResponse(InputStream in) throws IOException {
+		var status = readLine(in);
+		if (status == null)
+			throw new EOFException("Connection closed by server before a response status line was received.");
+		var chunked = false;
+		var contentLength = -1;
+		for (var line = readLine(in); line != null && ! line.isEmpty(); line = readLine(in)) {
+			var lc = line.toLowerCase(Locale.ROOT);
+			if (lc.startsWith("transfer-encoding:") && lc.contains("chunked"))
+				chunked = true;
+			else if (lc.startsWith("content-length:"))
+				contentLength = Integer.parseInt(lc.substring(15).trim());
+		}
+		var body = new ByteArrayOutputStream();
+		if (chunked) {
+			for (var size = Integer.parseInt(readLine(in).trim(), 16); size > 0; size = Integer.parseInt(readLine(in).trim(), 16)) {
+				body.write(in.readNBytes(size));
+				readLine(in);
+			}
+			readLine(in);
+		} else if (contentLength > 0) {
+			body.write(in.readNBytes(contentLength));
+		}
+		return status + " " + body.toString(UTF_8);
+	}
+
+	private static String readLine(InputStream in) throws IOException {
+		var sb = new StringBuilder();
+		for (var c = in.read(); c != -1; c = in.read()) {
+			if (c == '\n')
+				return sb.toString();
+			if (c != '\r')
+				sb.append((char)c);
+		}
+		return sb.isEmpty() ? null : sb.toString();
+	}
+}
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/HttpTransportBuilder.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/HttpTransportBuilder.java
index 7992663bd3..946408e4d6 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/HttpTransportBuilder.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/HttpTransportBuilder.java
@@ -27,9 +27,9 @@ package org.apache.juneau.rest.client;
  * <p>
  * Example usage:
  * <p class='bjava'>
- * 	HttpTransport <jv>transport</jv> = ApacheHc5Transport.builder()
- * 		.connectTimeout(Duration.ofSeconds(5))
- * 		.readTimeout(Duration.ofSeconds(30))
+ * 	<jc>// Transport-specific options (here, the underlying Apache client) are set on the concrete builder.</jc>
+ * 	HttpTransport <jv>transport</jv> = ApacheHc5Transport.<jsm>builder</jsm>()
+ * 		.httpClient(<jv>myCloseableHttpClient</jv>)
  * 		.build();
  *
  * 	RestClient <jv>client</jv> = RestClient.create()
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
index 8b5e72ceae..92404a8870 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestRequest.java
@@ -344,8 +344,12 @@ public final class RestRequest {
 	 * Sets a per-call response timeout for this request.
 	 *
 	 * <p>
-	 * Threaded onto the {@link TransportRequest} and applied by the transport as the response/read timeout.  Connect
-	 * timeouts remain a client-level setting.
+	 * Threaded onto the {@link TransportRequest}.  Connect timeouts remain a client-level setting.
+	 *
+	 * <p>
+	 * <b>Transport support:</b> currently only {@link JavaHttpTransport} applies this value (as the JDK
+	 * {@code HttpRequest} timeout).  The Apache HttpClient, OkHttp and Jetty transports do not consult it; configure
+	 * their own timeouts on the transport builder (e.g. {@code JettyHttpTransportBuilder.responseTimeoutMs(long)}).
 	 *
 	 * @param value The response timeout. May be <jk>null</jk> to use the transport default.
 	 * @return This object.
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/TransportRequest.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/TransportRequest.java
index 784d0a5080..7bbffefae6 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/TransportRequest.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/TransportRequest.java
@@ -147,8 +147,8 @@ public final class TransportRequest {
 	 * Returns the per-call response timeout, or {@code null} if none was set.
 	 *
 	 * <p>
-	 * Transports apply this as the response/read timeout for the request; a {@code null} value means the
-	 * transport's own default applies.  Connect timeouts remain a client-level concern.
+	 * A {@code null} value means the transport's own default applies.  Connect timeouts remain a client-level
+	 * concern.  Currently only {@link JavaHttpTransport} honors this value; other transports ignore it.
 	 *
 	 * @return The response timeout, possibly <jk>null</jk>.
 	 */
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteClient.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteClient.java
index 0895bf38dd..9eccfa86f4 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteClient.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/remote/RemoteClient.java
@@ -698,7 +698,7 @@ public final class RemoteClient {
 
 		private static boolean isBean(Object arg) {
 			return ! (arg instanceof CharSequence || arg instanceof Number || arg instanceof Boolean
-				|| arg instanceof Character || arg instanceof Enum || arg instanceof java.util.Date
+				|| arg instanceof Character || arg instanceof Enum || isDateTime(arg)
 				|| arg instanceof Collection || arg.getClass().isArray());
 		}
 
@@ -1146,7 +1146,12 @@ public final class RemoteClient {
 		/** Returns <jk>true</jk> if the {@code @Part} argument is a scalar that should be sent as a text field. */
 		private static boolean isScalarPart(Object arg) {
 			return arg instanceof CharSequence || arg instanceof Number || arg instanceof Boolean
-				|| arg instanceof Character || arg instanceof Enum || arg instanceof java.util.Date;
+				|| arg instanceof Character || arg instanceof Enum || isDateTime(arg);
+		}
+
+		/** Returns <jk>true</jk> for {@link java.util.Date} and {@code java.time} values (Instant, LocalDate, Duration, Period, ...), which are scalars. */
+		private static boolean isDateTime(Object arg) {
+			return arg instanceof java.util.Date || arg instanceof java.time.temporal.Temporal || arg instanceof java.time.temporal.TemporalAmount;
 		}
 
 		/**
diff --git a/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/RemoteClient_Test.java b/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/RemoteClient_Test.java
index 5527e1d214..961958ee1d 100644
--- a/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/RemoteClient_Test.java
+++ b/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/RemoteClient_Test.java
@@ -363,6 +363,32 @@ class RemoteClient_Test extends TestBase {
 		}
 	}
 
+	@Remote
+	interface JavaTimeQueryService {
+		@RemoteGet("/echo-query")
+		String getInstant(@Query java.time.Instant when);
+
+		@RemoteGet("/echo-query")
+		String getLocalDate(@Query java.time.LocalDate d);
+
+		@RemoteGet("/echo-query")
+		String getDuration(@Query java.time.Duration d);
+
+		@RemoteGet("/echo-query")
+		String getOffsetDateTime(@Query java.time.OffsetDateTime d);
+	}
+
+	@Test void g02_query_javaTimeArgs_sentAsScalars() throws Exception {
+		try (var client = RestClient.builder().rootUrl(rootUrl()).allowPrivateUrls(true).build()) {
+			var svc = client.remote(JavaTimeQueryService.class);
+			// Blank-named parts must serialize as a single scalar value, not expand as a bean.
+			assertTrue(svc.getInstant(java.time.Instant.parse("2026-01-02T03:04:05Z")).endsWith("=2026-01-02T03:04:05Z"));
+			assertTrue(svc.getLocalDate(java.time.LocalDate.of(2026, 1, 2)).endsWith("=2026-01-02"));
+			assertTrue(svc.getDuration(java.time.Duration.ofSeconds(5)).endsWith("=PT5S"));
+			assertTrue(svc.getOffsetDateTime(java.time.OffsetDateTime.of(2026, 1, 2, 3, 4, 5, 0, java.time.ZoneOffset.UTC)).endsWith("=2026-01-02T03:04:05Z"));
+		}
+	}
+
 	// -----------------------------------------------------------------------
 	// H — Object method delegation (toString, hashCode, equals on proxy)
 	// -----------------------------------------------------------------------
diff --git a/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/remote/RemoteClient_InvocationHandlerInternals_Test.java b/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/remote/RemoteClient_InvocationHandlerInternals_Test.java
index 7baf0b4f8b..f11c08f1e6 100644
--- a/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/remote/RemoteClient_InvocationHandlerInternals_Test.java
+++ b/juneau-rest/juneau-rest-client/src/test/java/org/apache/juneau/rest/client/remote/RemoteClient_InvocationHandlerInternals_Test.java
@@ -185,6 +185,19 @@ class RemoteClient_InvocationHandlerInternals_Test extends TestBase {
 		assertEquals(false, invokeStatic("isBean", new Class<?>[]{Object.class}, new int[]{1}));
 	}
 
+	@Test void e06b_isBean_javaTimeTypes_allFalse() throws Exception {
+		for (var v : javaTimeValues())
+			assertEquals(false, invokeStatic("isBean", new Class<?>[]{Object.class}, v), v.getClass().getName());
+	}
+
+	private static Object[] javaTimeValues() {
+		return new Object[]{
+			java.time.Instant.EPOCH, java.time.LocalDate.of(2026, 1, 2), java.time.LocalDateTime.of(2026, 1, 2, 3, 4),
+			java.time.OffsetDateTime.of(2026, 1, 2, 3, 4, 5, 0, java.time.ZoneOffset.UTC), java.time.ZonedDateTime.of(2026, 1, 2, 3, 4, 5, 0, java.time.ZoneOffset.UTC),
+			java.time.Duration.ofSeconds(5), java.time.Period.ofDays(3), java.time.LocalTime.NOON
+		};
+	}
+
 	private static Object TimeUnit() { return java.util.concurrent.TimeUnit.SECONDS; }
 
 	@Test void e07_isBean_plainObject_true() throws Exception {
@@ -336,6 +349,11 @@ class RemoteClient_InvocationHandlerInternals_Test extends TestBase {
 		assertEquals(true, invokeStatic("isScalarPart", new Class<?>[]{Object.class}, new Date()));
 	}
 
+	@Test void j03b_isScalarPart_javaTimeTypes_true() throws Exception {
+		for (var v : javaTimeValues())
+			assertEquals(true, invokeStatic("isScalarPart", new Class<?>[]{Object.class}, v), v.getClass().getName());
+	}
+
 	@Test void j04_isScalarPart_nonScalar_false() throws Exception {
 		assertEquals(false, invokeStatic("isScalarPart", new Class<?>[]{Object.class}, List.of("x")));
 		assertEquals(false, invokeStatic("isScalarPart", new Class<?>[]{Object.class}, new Object()));
diff --git a/juneau-rest/juneau-rest-server-console-ui-freemarker/src/main/java/org/apache/juneau/rest/server/view/freemarker/console/CardEnvelope.java b/juneau-rest/juneau-rest-server-console-ui-freemarker/src/main/java/org/apache/juneau/rest/server/view/freemarker/console/CardEnvelope.java
index e742c1e67c..3ebe31ab3a 100644
--- a/juneau-rest/juneau-rest-server-console-ui-freemarker/src/main/java/org/apache/juneau/rest/server/view/freemarker/console/CardEnvelope.java
+++ b/juneau-rest/juneau-rest-server-console-ui-freemarker/src/main/java/org/apache/juneau/rest/server/view/freemarker/console/CardEnvelope.java
@@ -57,13 +57,28 @@ final class CardEnvelope {
 		}
 	}
 
-	/** Author-catalog VIEW_META fields copied verbatim onto the lifted {@code view} when present. */
+	/**
+	 * Author-catalog VIEW_META fields copied verbatim onto the lifted {@code view} when present.
+	 *
+	 * <p>
+	 * Every view-level option {@code juneau-views.js} / {@code juneau-urlstate.js} reads off the view def must be listed
+	 * here, otherwise it is silently dropped on FTL-built pages: {@code defaultOrder}, {@code ribbon}, {@code dataMode},
+	 * {@code rowType}, {@code pollIntervalMs}, {@code columnConfig}, {@code cleanAddress}, {@code primary},
+	 * {@code copyLink} (set {@code false} to suppress the View Settings Copy-link button), {@code rowActions} (row-action
+	 * catalog), {@code rowClassRules}, and {@code pausePollingWhileEditing}.
+	 */
 	private static final Set<String> VIEW_META_PASSTHROUGH =
 		Set.of(
 			"defaultOrder", "ribbon", "dataMode", "rowType", "pollIntervalMs", "columnConfig",
-			"cleanAddress", "primary"
+			"cleanAddress", "primary", "copyLink", "rowActions", "rowClassRules", "pausePollingWhileEditing"
 		);
 
+	/**
+	 * Author-catalog SLOT_META (envelope-level, sibling of {@code view}) fields copied verbatim onto the lifted slot when
+	 * present: {@code selection} ({@code {rowIdField, selectAll}}) and {@code bulk} (the bulk-actions contract object).
+	 */
+	private static final Set<String> SLOT_META_PASSTHROUGH = Set.of("selection", "bulk");
+
 	/**
 	 * Lifts a {@code type="datatables"} author catalog into the frozen SLOT_META envelope that
 	 * {@code JuneauViews.init.mountTableSlot} handshakes (F2).
@@ -72,7 +87,9 @@ final class CardEnvelope {
 	 * The author writes IRS-portable catalog JSON5 (a bare {@code {dataUrl, columns:[{key,label}]}}),
 	 * <b>not</b> hand-authored VIEW_META. This method wraps it in a {@link ViewsMixin#SLOT_CONTRACT_VERSION}
 	 * slot carrying a {@link ViewsMixin#CONTRACT_VERSION} view: author {@code key}/{@code label} become
-	 * VIEW_META {@code data}/{@code title} ({@code data}/{@code title} are also accepted verbatim). An
+	 * VIEW_META {@code data}/{@code title} ({@code data}/{@code title} are also accepted verbatim). The view-level
+	 * options in {@link #VIEW_META_PASSTHROUGH} land on {@code view}; {@code selection} and {@code bulk} land on the slot
+	 * itself ({@link #SLOT_META_PASSTHROUGH}). An
 	 * object that already carries both {@code contractVersion} and {@code view} is treated as a
 	 * pre-built SLOT_META envelope and passed through unchanged (escape hatch).
 	 *
@@ -115,6 +132,9 @@ final class CardEnvelope {
 		slot.put("contractVersion", ViewsMixin.SLOT_CONTRACT_VERSION);
 		slot.put("layout", "wide");
 		slot.put("view", view);
+		for (var f : SLOT_META_PASSTHROUGH)
+			if (catalog.containsKey(f))
+				slot.put(f, catalog.get(f));
 		return slot;
 	}
 
diff --git a/juneau-rest/juneau-rest-server-console-ui-freemarker/src/main/java/org/apache/juneau/rest/server/view/freemarker/console/ToolkitPackRegistry.java b/juneau-rest/juneau-rest-server-console-ui-freemarker/src/main/java/org/apache/juneau/rest/server/view/freemarker/console/ToolkitPackRegistry.java
index 77c24202a0..70dc9b3eed 100644
--- a/juneau-rest/juneau-rest-server-console-ui-freemarker/src/main/java/org/apache/juneau/rest/server/view/freemarker/console/ToolkitPackRegistry.java
+++ b/juneau-rest/juneau-rest-server-console-ui-freemarker/src/main/java/org/apache/juneau/rest/server/view/freemarker/console/ToolkitPackRegistry.java
@@ -27,7 +27,10 @@ import org.apache.juneau.rest.server.widgets.*;
  *
  * <p>
  * Ships the {@code "views"} pack against the real {@link ViewsMixin} path constants. Load order is a
- * contract: CSS then JS, JS in the order the JRM {@code TableSlotPage} uses. A consumer can register
+ * contract: CSS then JS, JS in the order the JRM {@code TableSlotPage} uses (renders, icons, search, pagestate,
+ * urlstate, ribbon, [datatables glue], views, config, regions, helpers). {@code juneau-urlstate.js} is required
+ * by the View Settings Copy-link button and shareable-URL state ({@code JuneauViews.urlState}); it must precede
+ * {@code juneau-views.js} / {@code juneau-config.js}. A consumer can register
  * extra packs via {@code ConsoleFreemarkerMixin.Builder.registerToolkitPack(...)}.
  *
  * <p>
@@ -89,6 +92,7 @@ public final class ToolkitPackRegistry {
 				ViewsMixin.ICONS_JS_PATH,
 				ViewsMixin.SEARCH_JS_PATH,
 				ViewsMixin.PAGESTATE_JS_PATH,
+				ViewsMixin.URLSTATE_JS_PATH,
 				ViewsMixin.RIBBON_JS_PATH,
 				ViewsMixin.DATATABLES_JS_PATH,
 				ViewsMixin.VIEWS_JS_PATH,
diff --git a/juneau-rest/juneau-rest-server-console-ui-freemarker/src/test/java/org/apache/juneau/rest/server/view/freemarker/console/CardEnvelope_Test.java b/juneau-rest/juneau-rest-server-console-ui-freemarker/src/test/java/org/apache/juneau/rest/server/view/freemarker/console/CardEnvelope_Test.java
index f8acd13cde..49600d0f5f 100644
--- a/juneau-rest/juneau-rest-server-console-ui-freemarker/src/test/java/org/apache/juneau/rest/server/view/freemarker/console/CardEnvelope_Test.java
+++ b/juneau-rest/juneau-rest-server-console-ui-freemarker/src/test/java/org/apache/juneau/rest/server/view/freemarker/console/CardEnvelope_Test.java
@@ -60,6 +60,39 @@ class CardEnvelope_Test extends TestBase {
 		assertEquals("html", m.getString("type"));
 	}
 
+	private static final String BASE = "dataUrl:'/d', columns:[{key:'a',label:'A'}]";
+
+	@Test void liftTable_viewLevelKeys_roundTrip() {
+		var slot = CardEnvelope.liftTable("c1", CardEnvelope.parse("{" + BASE + """
+			, copyLink:false, primary:true, columnConfig:true,
+			rowActions:[{id:'del',label:'Delete'}],
+			rowClassRules:{'is-bad':'status==\\'bad\\''},
+			pausePollingWhileEditing:true
+			}"""));
+		assertBean(slot.getMap("view"), "id,copyLink,primary,columnConfig,pausePollingWhileEditing", "c1,false,true,true,true");
+		assertBean(slot.getMap("view").getList("rowActions").get(0), "id,label", "del,Delete");
+		assertTrue(slot.getMap("view").getMap("rowClassRules").containsKey("is-bad"));
+	}
+
+	@Test void liftTable_slotLevelKeys_selectionAndBulk() {
+		var slot = CardEnvelope.liftTable("c1", CardEnvelope.parse("{" + BASE + """
+			, selection:{rowIdField:'id', selectAll:false},
+			bulk:{contractVersion:1, actions:[{id:'x'}]}
+			}"""));
+		assertBean(slot.getMap("selection"), "rowIdField,selectAll", "id,false");
+		assertBean(slot.getMap("bulk"), "contractVersion", "1");
+		assertFalse(slot.getMap("view").containsKey("selection"));
+		assertFalse(slot.getMap("view").containsKey("bulk"));
+	}
+
+	@Test void liftTable_absentKeys_notEmitted() {
+		var slot = CardEnvelope.liftTable("c1", CardEnvelope.parse("{" + BASE + "}"));
+		for (var k : List.of("selection", "bulk"))
+			assertFalse(slot.containsKey(k), k);
+		for (var k : List.of("copyLink", "rowActions", "rowClassRules", "pausePollingWhileEditing"))
+			assertFalse(slot.getMap("view").containsKey(k), k);
+	}
+
 	// The new v1 main sources live here; the test CWD is the module directory (surefire).
 	private static final Path MAIN =
 		Path.of("src/main/java/org/apache/juneau/rest/server/view/freemarker/console");
diff --git a/juneau-rest/juneau-rest-server-console-ui-freemarker/src/test/java/org/apache/juneau/rest/server/view/freemarker/console/ToolkitPackRegistry_Test.java b/juneau-rest/juneau-rest-server-console-ui-freemarker/src/test/java/org/apache/juneau/rest/server/view/freemarker/console/ToolkitPackRegistry_Test.java
index 37a555e3f0..3ce4bacfb8 100644
--- a/juneau-rest/juneau-rest-server-console-ui-freemarker/src/test/java/org/apache/juneau/rest/server/view/freemarker/console/ToolkitPackRegistry_Test.java
+++ b/juneau-rest/juneau-rest-server-console-ui-freemarker/src/test/java/org/apache/juneau/rest/server/view/freemarker/console/ToolkitPackRegistry_Test.java
@@ -73,11 +73,12 @@ class ToolkitPackRegistry_Test extends TestBase {
 		assertTrue(indexOfContaining(r.cssUrls(), "juneau-views.css") >= 0, () -> r.cssUrls().toString());
 		assertTrue(indexOfContaining(r.cssUrls(), "juneau-config.css") >= 0, () -> r.cssUrls().toString());
 
-		// JS load order is a contract: renders, icons, search, pagestate, ribbon, datatables glue, views, config,
+		// JS load order is a contract: renders, icons, search, pagestate, urlstate (Copy link), ribbon, datatables glue, views, config,
 		// regions, helpers LAST.
 		var js = r.jsUrls();
 		var order = List.of(
-			"juneau-renders.js", "juneau-icons.js", "juneau-search.js", "juneau-pagestate.js", "juneau-ribbon.js",
+			"juneau-renders.js", "juneau-icons.js", "juneau-search.js", "juneau-pagestate.js", "juneau-urlstate.js",
+			"juneau-ribbon.js",
 			"juneau-datatables.js", "juneau-views.js", "juneau-config.js", "juneau-regions.js", "juneau-helpers.js");
 		var prev = -1;
 		for (var name : order) {
diff --git a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
index a1a60bfac4..1071a33c29 100644
--- a/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
+++ b/juneau-rest/juneau-rest-server-console-ui/src/main/java/org/apache/juneau/rest/server/console/ConsoleChromeMixin.java
@@ -174,7 +174,7 @@ public class ConsoleChromeMixin {
 	public static final String CONSOLE_JS_PATH = "/juneau-console/juneau-console.js";
 
 	/** {@link #CONSOLE_JS_PATH} minus the {@code /juneau-console} prefix - see the class javadoc's mount-styles section. */
-	static final String CONSOLE_JS_PATH_UNPREFIXED = "/juneau-console.js";
+	public static final String CONSOLE_JS_PATH_UNPREFIXED = "/juneau-console.js";
 
 	/** Classpath location of the console shell script. */
 	public static final String CONSOLE_JS_RESOURCE = "/org/apache/juneau/console/juneau-console.js";
diff --git a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/Render.java b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/Render.java
index 0d733583e5..a466654511 100644
--- a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/Render.java
+++ b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/Render.java
@@ -24,12 +24,15 @@ import org.apache.juneau.commons.bean.*;
 import org.apache.juneau.rest.server.widgets.*;
 
 /**
- * A named cell-renderer reference in the {@code VIEW_META} wire contract (design doc §6.6).
+ * A named cell-renderer reference in the {@code VIEW_META} wire contract.
  *
  * <p>
- * Serializes to the canonical object form <c>{"id":"tag","meta":{"field":"status"}}</c>.  The client
+ * Serializes to the canonical object form <c>{"id":"tag","meta":{"field":"status"}}</c>; the optional
+ * {@link #meta} and {@link #popover} members are omitted from the wire when unset.  The client
  * {@code juneau-renders.js} registry looks up {@link #id} and invokes the matching renderer, passing the
- * {@link #meta} map through as per-column context.  Timestamp renderers ({@code ts-zulu}, {@code datetime})
+ * {@link #meta} map through as per-column context (and painting {@link #popover}, when present, from row data
+ * already on the client).  Typed factories exist for the renderers that take structured metadata:
+ * {@link #progress(int)} and {@link #pill(String)}.  Timestamp renderers ({@code ts-zulu}, {@code datetime})
  * honor {@code meta.popup}: {@code off} disables the local/California hover popup; {@code ts-zulu} defaults
  * the popup on, {@code datetime} defaults it off.
  *
diff --git a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
index ba97954ab4..9de1144e75 100644
--- a/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
+++ b/juneau-rest/juneau-rest-server-views/src/main/java/org/apache/juneau/rest/server/views/ViewsMixin.java
@@ -309,14 +309,14 @@ public class ViewsMixin {
 	 *
 	 * <h5 class='section'>Example:</h5>
 	 * <p class='bjava'>
-	 * 	<jc>// A views-only application serves the console shell at the same stable path ConsoleChromeMixin uses.</jc>
+	 * 	<jc>// A views-only application serves the console shell at the same stable (unprefixed) path ConsoleChromeMixin serves it at.</jc>
 	 * 	<ja>&#64;Rest</ja>(mixins=ViewsMixin.<jk>class</jk>)
 	 * 	<jk>public class</jk> MyResource {}
 	 *
 	 * 	<jc>// GET /juneau-console.js now returns the shell runtime.</jc>
 	 * </p>
 	 */
-	public static final String CONSOLE_JS_PATH = "/juneau-console.js";
+	public static final String CONSOLE_JS_PATH = ConsoleChromeMixin.CONSOLE_JS_PATH_UNPREFIXED;
 
 	/**
 	 * The frozen {@code VIEW_META} contract-version handshake constant. FTL {@code <@card type="datatables">}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/DataTablesAjax_BrowserTest.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/DataTablesAjax_BrowserTest.java
index 0588237dd0..9439277ca4 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/DataTablesAjax_BrowserTest.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/DataTablesAjax_BrowserTest.java
@@ -28,6 +28,7 @@ import java.util.concurrent.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.commons.beanquery.*;
+import org.apache.juneau.http.BeanQueryRequest;
 import org.apache.juneau.marshall.marshaller.*;
 import org.apache.juneau.rest.server.datatables.*;
 import org.apache.juneau.rest.server.datatables.adapter.*;
@@ -118,6 +119,8 @@ class DataTablesAjax_BrowserTest extends TestBase {
 				try (var os = ex.getResponseBody()) {
 					os.write(bytes);
 				}
+			} catch (BeanQuerySyntaxException e) {
+				sendBadRequest(ex, e);
 			} catch (Exception e) {
 				sendFailure(ex, e);
 			}
@@ -133,6 +136,20 @@ class DataTablesAjax_BrowserTest extends TestBase {
 		server.start();
 	}
 
+	/**
+	 * Answers a query syntax error the way {@code RestContext.convertThrowable} does: a 400 carrying the error code in
+	 * the {@code X-BeanQuery-Error} header and the message as the body.
+	 */
+	private static void sendBadRequest(HttpExchange ex, BeanQuerySyntaxException e) throws IOException {
+		var bytes = e.getMessage().getBytes(UTF_8);
+		ex.getResponseHeaders().add(BeanQueryRequest.ERROR_HEADER, e.code().name());
+		ex.getResponseHeaders().add("Content-Type", "text/plain;charset=utf-8");
+		ex.sendResponseHeaders(400, bytes.length);
+		try (var os = ex.getResponseBody()) {
+			os.write(bytes);
+		}
+	}
+
 	/**
 	 * Answers a handler failure with a 500 carrying the stack trace, instead of throwing (which makes the JDK server
 	 * drop the connection with no response) - so the prober's network diagnostics show why a draw failed.
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
index 34bf39f2c2..4014328a33 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ViewsMixin_Serving_Test.java
@@ -1224,6 +1224,11 @@ class ViewsMixin_Serving_Test extends TestBase {
 			.assertContent().asString().isContains("window.JuneauConsole");
 	}
 
+	@Test void q03_consoleJsPath_singleSourceOfTruth() {
+		assertEquals("/juneau-console.js", ViewsMixin.CONSOLE_JS_PATH);
+		assertEquals(org.apache.juneau.rest.server.console.ConsoleChromeMixin.CONSOLE_JS_PATH_UNPREFIXED, ViewsMixin.CONSOLE_JS_PATH);
+	}
+
 	@Test void q02_hostWithoutMixin_consoleJsRouteIs404() throws Exception {
 		cNoMixin.get(ViewsMixin.CONSOLE_JS_PATH).run().assertStatus(404);
 	}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/js/datatables-ajax.cjs b/juneau-rest/juneau-rest-server-views/src/test/js/datatables-ajax.cjs
index 50965ea493..97d87b4601 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/js/datatables-ajax.cjs
+++ b/juneau-rest/juneau-rest-server-views/src/test/js/datatables-ajax.cjs
@@ -133,11 +133,11 @@ const PROBE = async function () {
 	out.t1Page2 = { firstRow: firstRowText('t1', 1) };
 
 	// 6) #t3 has a bad column (data: 'nope'); its draws surface the server's error via DataTables' default
-	// errMode dialog (window.alert), captured by the harness's page.on('dialog') listener below.  DataTables fires
-	// dt-error just before that alert, which is what this step waits on.  Searching the column makes the failure
-	// independent of the initial-order draw.
+	// errMode dialog (window.alert), captured by the harness's page.on('dialog') listener below.  DataTables
+	// (dt-error) or the Juneau glue (error.dt) fires its event just before that alert, which is what this step
+	// waits on.  Searching the column makes the failure independent of the initial-order draw.
 	let t3Errored = false;
-	window.jQuery('#t3').on('dt-error.dt', () => { t3Errored = true; });
+	window.jQuery('#t3').on('error.dt dt-error.dt', () => { t3Errored = true; });
 	window.JuneauDataTables_t3.table.column(0).search('x').draw();
 	await waitFor('t3Error', () => t3Errored, 10000);
 	await sleep(100); // let the alert itself reach page.on('dialog')
@@ -160,6 +160,12 @@ const PROBE = async function () {
 		const dialogs = [];
 		page.on('pageerror', e => failures.push(String(e)));
 		page.on('console', m => { if (m.type() === 'error') failures.push(m.text()); });
+		// #t3 (the only table sending X-Test-Header) deliberately draws a bad column, so the server answers 400 and the
+		// browser logs one "Failed to load resource" console error per response.  Those are expected; anything else is not.
+		let expectedBadColumn400s = 0;
+		page.on('response', r => {
+			if (r.status() === 400 && r.request().headers()['x-test-header']) expectedBadColumn400s++;
+		});
 		page.on('dialog', async d => { dialogs.push(d.message()); await d.dismiss(); });
 
 		let report;
@@ -176,7 +182,12 @@ const PROBE = async function () {
 			dumpDiagnostics(diag);
 			throw error;
 		}
-		report.jsFailures = failures.slice();
+		const is400 = f => /Failed to load resource.*\b400\b/.test(f);
+		const unexpected = [];
+		const expected = [];
+		for (const f of failures) (is400(f) && expected.length < expectedBadColumn400s ? expected : unexpected).push(f);
+		report.jsFailures = unexpected;
+		report.expectedBadColumnFailures = expected;
 		report.diagnostics = diag.slice();
 		report.dialogs = dialogs.slice();
 		process.stdout.write(JSON.stringify(report, null, 2) + '\n');
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/arg/RestOpArg.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/arg/RestOpArg.java
index 007bb7c6ff..675e4febb1 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/arg/RestOpArg.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/arg/RestOpArg.java
@@ -26,7 +26,8 @@ import org.apache.juneau.rest.server.*;
  *
  * <h5 class='figure'>Example:</h5>
  * <p class='bjava'>
- * 	<jc>// A simple parameter resolver that resolves TimeZone parameters.</jc>
+ * 	<jc>// A simple parameter resolver that resolves TimeZone parameters from the Time-Zone header.</jc>
+ * 	<jc>// (TimeZone parameters are already resolved out-of-the-box; this is purely illustrative.)</jc>
  * 	<jk>public class</jk> TimeZoneArg <jk>implements</jk> RestOpArg {
  *
  * 		<jc>// Implementers must provide a static creator method that returns a RestParam if it's
@@ -41,7 +42,7 @@ import org.apache.juneau.rest.server.*;
  *
  * 		<ja>@Override</ja>
  * 		<jk>public</jk> Object resolve(RestOpSession <jv>opSession</jv>) <jk>throws</jk> Exception {
- * 			<jk>return</jk> <jv>opSession</jv>.getRequest().getHeaders().getTimeZone();
+ * 			<jk>return</jk> <jv>opSession</jv>.getRequest().getTimeZone().<jsm>orElse</jsm>(<jk>null</jk>);
  * 		}
  * 	}
  * </p>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestContent.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestContent.java
index 9db09d34e9..0f20990fa1 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestContent.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/httppart/RequestContent.java
@@ -594,6 +594,7 @@ public class RequestContent {
 
 			try (Closeable in = session.isReaderParser() ? getUnbufferedReader() : getInputStream()) {
 				var o = session.read(in, cm);
+				drain(in);
 				if (nn(schema))
 					schema.validateOutput(o);
 				return o;
@@ -616,6 +617,33 @@ public class RequestContent {
 			ct.isPresent() ? ct.get().asMediaType().orElse(null) : "not-specified", Json5.of(req.getOpContext().getParsers().getSupportedMediaTypes()));
 	}
 
+	// Reads and discards whatever remains of the request entity after a parser has returned.
+	//
+	// A parser stops as soon as the top-level value is complete, leaving the end of the entity unread: for a
+	// Transfer-Encoding: chunked request, at least the zero-length terminating chunk whenever the client flushes it
+	// separately from the content.  If that is still unread when the response completes, the container finds
+	// unconsumed request content and aborts the connection.  By then the response is usually committed, so no
+	// Connection: close can be added, and a client reusing the pooled keep-alive connection fails its next request
+	// with an immediate EOF.  Draining here (blocking until end-of-stream) keeps the connection reusable.
+	//
+	// Best effort: the stream is still bounded by @Rest(maxInput), and any failure (limit exceeded, client gone)
+	// just falls back to the container's own unconsumed-content handling.  A zero-byte read also stops the loop,
+	// since BoundedServletInputStream returns 0 rather than -1 once its limit is reached.
+	// Q: Can we move this to IOUtils?
+	private static void drain(Closeable in) {
+		try {
+			if (in instanceof Reader r) {
+				var buf = new char[256];
+				while (r.read(buf) > 0) { /* discard */ }
+			} else if (in instanceof InputStream is) {
+				var buf = new byte[256];
+				while (is.read(buf) > 0) { /* discard */ }
+			}
+		} catch (IOException ignored) {
+			// best-effort; the parsed value is already complete
+		}
+	}
+
 	/**
 	 * Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader};
 	 *