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 2020/04/04 14:14:01 UTC

[juneau] branch master updated: RestClient tests

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 ada5eef  RestClient tests
ada5eef is described below

commit ada5eef63907c073887bf108cdfd3c509f679cdc
Author: JamesBognar <ja...@apache.org>
AuthorDate: Sat Apr 4 10:13:42 2020 -0400

    RestClient tests
---
 .../org/apache/juneau/BasicAssertionError.java     |  63 ++++
 .../apache/juneau/http/annotation/FormData.java    |  17 +
 .../org/apache/juneau/http/annotation/Header.java  |  17 +
 .../org/apache/juneau/http/annotation/Query.java   |  17 +
 juneau-doc/docs/ReleaseNotes/8.1.4.html            |   7 +
 .../27.OpenApiDetails/02.Serializers.html          |   8 +-
 .../juneau/rest/test/client/RestClientTest.java    |   4 +-
 .../juneau/rest/client2/RestClientBuilderTest.java |  76 ++--
 .../apache/juneau/rest/client2/RestResponse.java   |  14 +-
 .../juneau/rest/client2/RestResponseBody.java      |  40 +-
 .../juneau/rest/client2/RestResponseHeader.java    |  47 ++-
 juneau-rest/juneau-rest-server/pom.xml             |   4 +
 .../apache/juneau/rest/BasicRestCallHandler.java   |   3 +
 .../org/apache/juneau/rest/RequestFormData.java    | 419 ++++++++++++++++++---
 .../org/apache/juneau/rest/RequestHeader.java}     | 133 +++----
 .../org/apache/juneau/rest/RequestHeaders.java     | 147 +++++++-
 .../java/org/apache/juneau/rest/RequestPath.java   |  10 +-
 .../java/org/apache/juneau/rest/RequestQuery.java  |  14 +-
 .../main/java/org/apache/juneau/rest/RestCall.java |  11 +
 .../java/org/apache/juneau/rest/RestContext.java   |  18 +-
 .../org/apache/juneau/rest/RestParamDefaults.java  |  51 ++-
 .../java/org/apache/juneau/rest/RestRequest.java   |   8 +
 .../java/org/apache/juneau/rest/RestResponse.java  |  62 ++-
 .../juneau/rest/reshandlers/DefaultHandler.java    |   2 +
 pom.xml                                            |   6 +
 25 files changed, 945 insertions(+), 253 deletions(-)

diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BasicAssertionError.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BasicAssertionError.java
new file mode 100644
index 0000000..63a6ba5
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BasicAssertionError.java
@@ -0,0 +1,63 @@
+// ***************************************************************************************************************************
+// * 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.internal.StringUtils.*;
+
+import java.text.*;
+
+/**
+ * An extension of {@link AssertionError} with helper constructors for messages with message-style arguments.
+ */
+public class BasicAssertionError extends AssertionError {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param message The {@link MessageFormat}-style message.
+	 * @param args Optional {@link MessageFormat}-style arguments.
+	 */
+	public BasicAssertionError(String message, Object...args) {
+		super(format(message, args));
+	}
+
+	/**
+	 * Constructor.
+	 *
+	 * @param cause The cause of this exception.
+	 * @param message The {@link MessageFormat}-style message.
+	 * @param args Optional {@link MessageFormat}-style arguments.
+	 */
+	public BasicAssertionError(Throwable cause, String message, Object...args) {
+		this(getMessage(cause, message, null), args);
+		initCause(cause);
+	}
+
+	/**
+	 * Finds the message.
+	 *
+	 * @param cause The cause.
+	 * @param msg The message.
+	 * @param def The default value if both above are <jk>null</jk>.
+	 * @return The resolved message.
+	 */
+	protected static final String getMessage(Throwable cause, String msg, String def) {
+		if (msg != null)
+			return msg;
+		if (cause != null)
+			return cause.getMessage();
+		return def;
+	}
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/FormData.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/FormData.java
index 8c2be4d..d3e3d31 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/FormData.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/FormData.java
@@ -147,6 +147,23 @@ public @interface FormData {
 	 */
 	Class<? extends HttpPartParser> parser() default HttpPartParser.Null.class;
 
+	/**
+	 * Denotes a multi-part parameter (e.g. multiple entries with the same name).
+	 *
+	 * <h5 class='figure'>Example</h5>
+	 * 	<jk>public void</jk> doPost(
+	 * 		<ja>@FormData</ja>(name=<js>"beans"</js>, multi=<jk>true</jk>) MyBean[] beans
+	 * 	) {
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		Meant to be used on multi-part parameters (e.g. <js>"key=1&amp;key=2&amp;key=3"</js> instead of <js>"key=@(1,2,3)"</js>)
+	 * 	<li>
+	 * 		The data type must be a collection or array type.
+	 * </ul>
+	 */
+	boolean multi() default false;
+
 	//=================================================================================================================
 	// Attributes common to all Swagger Parameter objects
 	//=================================================================================================================
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Header.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Header.java
index 3aa1388..45dc38c 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Header.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Header.java
@@ -120,6 +120,23 @@ public @interface Header {
 	 */
 	Class<? extends HttpPartParser> parser() default HttpPartParser.Null.class;
 
+	/**
+	 * Denotes a multi-part parameter (e.g. multiple entries with the same name).
+	 *
+	 * <h5 class='figure'>Example</h5>
+	 * 	<jk>public void</jk> doPost(
+	 * 		<ja>@Header</ja>(name=<js>"Beans"</js>, multi=<jk>true</jk>) MyBean[] beans
+	 * 	) {
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		Meant to be used on multi-part parameters (e.g. <js>"Header: h1"</js>, <js>"Header: h2"</js> instead of <js>"Header: @(h1,h2)"</js>)
+	 * 	<li>
+	 * 		The data type must be a collection or array type.
+	 * </ul>
+	 */
+	boolean multi() default false;
+
 	//=================================================================================================================
 	// Attributes common to all Swagger Parameter objects
 	//=================================================================================================================
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Query.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Query.java
index a7a1db7..b7704a7 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Query.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/annotation/Query.java
@@ -128,6 +128,23 @@ public @interface Query {
 	 */
 	Class<? extends HttpPartParser> parser() default HttpPartParser.Null.class;
 
+	/**
+	 * Denotes a multi-part parameter (e.g. multiple entries with the same name).
+	 *
+	 * <h5 class='figure'>Example</h5>
+	 * 	<jk>public void</jk> doPost(
+	 * 		<ja>@Query</ja>(name=<js>"beans"</js>, multi=<jk>true</jk>) MyBean[] beans
+	 * 	) {
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		Meant to be used on multi-part parameters (e.g. <js>"key=1&amp;key=2&amp;key=3"</js> instead of <js>"key=@(1,2,3)"</js>)
+	 * 	<li>
+	 * 		The data type must be a collection or array type.
+	 * </ul>
+	 */
+	boolean multi() default false;
+
 	//=================================================================================================================
 	// Attributes common to all Swagger Parameter objects
 	//=================================================================================================================
diff --git a/juneau-doc/docs/ReleaseNotes/8.1.4.html b/juneau-doc/docs/ReleaseNotes/8.1.4.html
index 984bb0d..49a3475 100644
--- a/juneau-doc/docs/ReleaseNotes/8.1.4.html
+++ b/juneau-doc/docs/ReleaseNotes/8.1.4.html
@@ -176,6 +176,13 @@
 	}
 		</p>		
 	<li>
+		New annotations for multi-part support:
+		<ul>
+			<li class='jm'>{@link oaj.http.annotation.Header#multi()}
+			<li class='jm'>{@link oaj.http.annotation.Query#multi()}
+			<li class='jm'>{@link oaj.http.annotation.FormData#multi()}
+		</ul>
+	<li>
 		HTML-Schema support is being deprecated due to low-use and difficulty in maintaining.  It will be removed in 9.0.
 </ul>
 
diff --git a/juneau-doc/docs/Topics/02.juneau-marshall/27.OpenApiDetails/02.Serializers.html b/juneau-doc/docs/Topics/02.juneau-marshall/27.OpenApiDetails/02.Serializers.html
index 29a268e..2bca333 100644
--- a/juneau-doc/docs/Topics/02.juneau-marshall/27.OpenApiDetails/02.Serializers.html
+++ b/juneau-doc/docs/Topics/02.juneau-marshall/27.OpenApiDetails/02.Serializers.html
@@ -51,14 +51,12 @@ OpenAPI Serializers
 	Under-the-covers, this gets converted to the following schema object:
 </p>
 <p class='bpcode w800'>
-	<jk>import static</jk> org.apache.juneau.httppart.HttpPartSchema.*;
-
-	HttpPartSchema schema = <jsm>create</jsm>()
+	HttpPartSchema schema = HttpPartSchema.<jsm>create</jsm>()
 		.items(
-			<jsm>create</jsm>()
+			HttpPartSchema.<jsm>create</jsm>()
 				.collectionFormat(<js>"pipes"</js>)
 				.items(
-					<jsm>create</jsm>()
+					HttpPartSchema.<jsm>create</jsm>()
 						.collectionFormat(<js>"csv"</js>)
 						.type(<js>"integer"</js>) 
 						.format(<js>"int64"</js>)
diff --git a/juneau-microservice/juneau-microservice-ftest/src/test/java/org/apache/juneau/rest/test/client/RestClientTest.java b/juneau-microservice/juneau-microservice-ftest/src/test/java/org/apache/juneau/rest/test/client/RestClientTest.java
index fe1ca75..17779fe 100644
--- a/juneau-microservice/juneau-microservice-ftest/src/test/java/org/apache/juneau/rest/test/client/RestClientTest.java
+++ b/juneau-microservice/juneau-microservice-ftest/src/test/java/org/apache/juneau/rest/test/client/RestClientTest.java
@@ -43,7 +43,7 @@ public class RestClientTest extends RestTestcase {
 		try {
 			c.post(URL, new StringEntity("xxxFAILURExxx")).run().getBody().assertContains("SUCCESS");
 			fail();
-		} catch (RestCallException e) {
+		} catch (AssertionError e) {
 			assertTrue(e.getLocalizedMessage().contains("Response did not have the expected substring for body."));
 		}
 	}
@@ -64,7 +64,7 @@ public class RestClientTest extends RestTestcase {
 		try {
 			c.post(URL, new StringEntity("xxxFAILURExxx")).run().getBody().assertValue(x -> ! x.contains("FAILURE"));
 			fail();
-		} catch (RestCallException e) {
+		} catch (AssertionError e) {
 			assertTrue(e.getLocalizedMessage().contains("Response did not have the expected value for body."));
 		}
 	}
diff --git a/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RestClientBuilderTest.java b/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RestClientBuilderTest.java
index 07d020f..7faca72 100644
--- a/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RestClientBuilderTest.java
+++ b/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RestClientBuilderTest.java
@@ -33,6 +33,7 @@ import org.apache.juneau.*;
 import org.apache.juneau.collections.*;
 import org.apache.juneau.http.*;
 import org.apache.juneau.http.annotation.*;
+import org.apache.juneau.http.annotation.Header;
 import org.apache.juneau.http.exception.*;
 import org.apache.juneau.http.response.*;
 import org.apache.juneau.httppart.*;
@@ -2182,30 +2183,57 @@ public class RestClientBuilderTest {
 		}
 	}
 
-//	@Test
-//	public void k17_restClient_partParserClass() throws Exception {
-//		RestClient rc = MockRestClient
-//			.create(A.class)
-//			.simpleJson()
-//			.serializers(XmlSerializer.DEFAULT,JsonSerializer.DEFAULT)
-//			.parsers(XmlParser.DEFAULT,JsonParser.DEFAULT)
-//			.build();
-//
-//	}
-////	public RestClientBuilder partParser(Class<? extends HttpPartParser> value) {
-//
-//	@Test
-//	public void k18_restClient_partParserObject() throws Exception { fail(); }
-////	public RestClientBuilder partParser(HttpPartParser value) {
-//
-//	@Test
-//	public void k19_restClient_partSerializerClass() throws Exception { fail(); }
-////	public RestClientBuilder partSerializer(Class<? extends HttpPartSerializer> value) {
-//
-//	@Test
-//	public void k20_restClient_partSerializerObject() throws Exception { fail(); }
-////	public RestClientBuilder partSerializer(HttpPartSerializer value) {
-//
+	@Rest(partSerializer=XPartSerializer.class, partParser=XPartParser.class,debug="true")
+	public static class K extends BasicRest {
+		@RestMethod(path="/")
+		public Ok get(@Header(name="Foo",multi=true) Bean[] foo, org.apache.juneau.rest.RestRequest req, org.apache.juneau.rest.RestResponse res) throws Exception {
+			assertEquals(2, foo.length);
+			assertObjectEquals("['x{f:1}','x{f:1}']", req.getHeaders().getAll("Foo", String[].class));
+			assertEquals("{f:1}", foo[0].toString());
+			assertEquals("{f:1}", foo[1].toString());
+			res.header("Foo", bean);
+			return Ok.OK;
+		}
+	}
+
+	@Test
+	public void k17_restClient_partSerializer_partParser_Class() throws Exception {
+		RestClient rc = MockRestClient
+			.create(K.class)
+			.simpleJson()
+			.header("Foo",bean)
+			.partSerializer(XPartSerializer.class)
+			.partParser(XPartParser.class)
+			.header("")
+			.build();
+		Bean b = rc
+			.get("/")
+			.header("Foo",bean)
+			.run()
+			.getHeader("Foo").assertValue("x{f:1}")
+			.getHeader("Foo").as(Bean.class);
+		assertEquals("{f:1}", b.toString());
+	}
+
+	@Test
+	public void k18_restClient_partSerializer_partParser_Object() throws Exception {
+		RestClient rc = MockRestClient
+			.create(K.class)
+			.simpleJson()
+			.header("Foo",bean)
+			.partSerializer(new XPartSerializer())
+			.partParser(new XPartParser())
+			.header("")
+			.build();
+		Bean b = rc
+			.get("/")
+			.header("Foo",bean)
+			.run()
+			.getHeader("Foo").assertValue("x{f:1}")
+			.getHeader("Foo").as(Bean.class);
+		assertEquals("{f:1}", b.toString());
+	}
+
 //	@Test
 //	public void k21_restClient_serializerClass() throws Exception { fail(); }
 ////	public RestClientBuilder serializer(Class<? extends Serializer> value) {
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponse.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponse.java
index 3618bc4..e2fb349 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponse.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponse.java
@@ -154,14 +154,15 @@ public final class RestResponse implements HttpResponse {
 	 *
 	 * @param validCodes The list of valid codes.
 	 * @return This object (for method chaining).
-	 * @throws RestCallException If assertion fails.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertStatusCode(int...validCodes) throws RestCallException {
+	public RestResponse assertStatusCode(int...validCodes) throws RestCallException, AssertionError {
 		int sc = getStatusCode();
 		for (int c : validCodes)
 			if (c == sc)
 				return this;
-		throw new RestCallException("Response did not have the expected status code.\n\tExpected=[{0}]\n\tActual=[{1}]", validCodes, sc);
+		throw new BasicAssertionError("Response did not have the expected status code.\n\tExpected=[{0}]\n\tActual=[{1}]", validCodes, sc);
 	}
 
 	/**
@@ -169,13 +170,14 @@ public final class RestResponse implements HttpResponse {
 	 *
 	 * @param test The test.
 	 * @return This object (for method chaining).
-	 * @throws RestCallException If assertion fails.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertStatusCode(Predicate<Integer> test) throws RestCallException {
+	public RestResponse assertStatusCode(Predicate<Integer> test) throws RestCallException, AssertionError {
 		int sc = getStatusCode();
 		if (test.test(sc))
 			return this;
-		throw new RestCallException("Response did not have the expected status code.\n\tActual=[{0}]", sc);
+		throw new BasicAssertionError("Response did not have the expected status code.\n\tActual=[{0}]", sc);
 	}
 
 	//------------------------------------------------------------------------------------------------------------------
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseBody.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseBody.java
index 4ddbd30..5472c3e 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseBody.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseBody.java
@@ -1542,12 +1542,13 @@ public class RestResponseBody implements HttpEntity {
 	 *
 	 * @param value The value to check against.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion fails.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertValue(String value) throws RestCallException {
+	public RestResponse assertValue(String value) throws RestCallException, AssertionError {
 		String text = asString();
 		if (! StringUtils.isEquals(value, text))
-			throw new RestCallException("Response did not have the expected value for body.\n\tExpected=[{0}]\n\tActual=[{1}]", value, text);
+			throw new BasicAssertionError("Response did not have the expected value for body.\n\tExpected=[{0}]\n\tActual=[{1}]", value, text);
 		return response;
 	}
 
@@ -1576,13 +1577,14 @@ public class RestResponseBody implements HttpEntity {
 	 *
 	 * @param values The values to check against.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion fails.
+	 * @throws AssertionError If assertion failed.
+	 * @throws RestCallException If REST call failed.
 	 */
-	public RestResponse assertContains(String...values) throws RestCallException {
+	public RestResponse assertContains(String...values) throws RestCallException, AssertionError {
 		String text = asString();
 		for (String substring : values)
 			if (! StringUtils.contains(text, substring))
-				throw new RestCallException("Response did not have the expected substring for body.\n\tExpected=[{0}]\n\tBody=[{1}]", substring, text);
+				throw new BasicAssertionError("Response did not have the expected substring for body.\n\tExpected=[{0}]\n\tBody=[{1}]", substring, text);
 		return response;
 	}
 
@@ -1611,12 +1613,13 @@ public class RestResponseBody implements HttpEntity {
 	 *
 	 * @param test The predicate to use to test the body context.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion fails.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertValue(Predicate<String> test) throws RestCallException {
+	public RestResponse assertValue(Predicate<String> test) throws RestCallException, AssertionError {
 		String text = asString();
 		if (! test.test(text))
-			throw new RestCallException("Response did not have the expected value for body.\n\tActual=[{0}]", text);
+			throw new BasicAssertionError("Response did not have the expected value for body.\n\tActual=[{0}]", text);
 		return response;
 	}
 
@@ -1645,9 +1648,10 @@ public class RestResponseBody implements HttpEntity {
 	 *
 	 * @param regex The pattern to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion fails.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertMatches(String regex) throws RestCallException {
+	public RestResponse assertMatches(String regex) throws RestCallException, AssertionError {
 		return assertMatches(regex, 0);
 	}
 
@@ -1677,13 +1681,14 @@ public class RestResponseBody implements HttpEntity {
 	 * @param regex The pattern to test for.
 	 * @param flags Pattern match flags.  See {@link Pattern#compile(String, int)}.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion fails.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertMatches(String regex, int flags) throws RestCallException {
+	public RestResponse assertMatches(String regex, int flags) throws RestCallException, AssertionError {
 		String text = asString();
 		Pattern p = Pattern.compile(regex, flags);
 		if (! p.matcher(text).matches())
-			throw new RestCallException("Response did not match expected pattern.\n\tpattern=[{0}]\n\tBody=[{1}]", regex, text);
+			throw new BasicAssertionError("Response did not match expected pattern.\n\tpattern=[{0}]\n\tBody=[{1}]", regex, text);
 		return response;
 	}
 
@@ -1713,12 +1718,13 @@ public class RestResponseBody implements HttpEntity {
 	 *
 	 * @param pattern The pattern to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion fails.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertMatches(Pattern pattern) throws RestCallException {
+	public RestResponse assertMatches(Pattern pattern) throws RestCallException, AssertionError {
 		String text = asString();
 		if (! pattern.matcher(text).matches())
-			throw new RestCallException("Response did not match expected pattern.\n\tpattern=[{0}]\n\tBody=[{1}]", pattern.pattern(), text);
+			throw new BasicAssertionError("Response did not match expected pattern.\n\tpattern=[{0}]\n\tBody=[{1}]", pattern.pattern(), text);
 		return response;
 	}
 
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseHeader.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseHeader.java
index 0023f46..650bbe7 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseHeader.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseHeader.java
@@ -516,11 +516,12 @@ public class RestResponseHeader implements Header {
 	 * </p>
 	 *
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertExists() throws RestCallException {
+	public RestResponse assertExists() throws RestCallException, AssertionError {
 		if (! exists())
-			throw new RestCallException("Response did not have the expected header {0}.", getName());
+			throw new BasicAssertionError("Response did not have the expected header {0}.", getName());
 		return response;
 	}
 
@@ -538,11 +539,12 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param value The value to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertValue(String value) throws RestCallException {
+	public RestResponse assertValue(String value) throws RestCallException, AssertionError {
 		if (! StringUtils.isEquals(value, asString()))
-			throw new RestCallException("Response did not have the expected value for header {0}.\n\tExpected=[{1}]\n\tActual=[{2}]", getName(), value, asString());
+			throw new BasicAssertionError("Response did not have the expected value for header {0}.\n\tExpected=[{1}]\n\tActual=[{2}]", getName(), value, asString());
 		return response;
 	}
 
@@ -560,12 +562,13 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param test The predicate to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertValue(Predicate<String> test) throws RestCallException {
+	public RestResponse assertValue(Predicate<String> test) throws RestCallException, AssertionError {
 		String text = asString();
 		if (! test.test(text))
-			throw new RestCallException("Response did not have the expected value for header {0}.\n\tActual=[{1}]", getName(), text);
+			throw new BasicAssertionError("Response did not have the expected value for header {0}.\n\tActual=[{1}]", getName(), text);
 		return response;
 	}
 
@@ -583,13 +586,14 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param values The substrings to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertContains(String...values) throws RestCallException {
+	public RestResponse assertContains(String...values) throws RestCallException, AssertionError {
 		String text = asString();
 		for (String substring : values)
 			if (! StringUtils.contains(text, substring))
-				throw new RestCallException("Response did not have the expected substring in header {0}.\n\tExpected=[{1}]\n\tHeader=[{2}]", getName(), substring, text);
+				throw new BasicAssertionError("Response did not have the expected substring in header {0}.\n\tExpected=[{1}]\n\tHeader=[{2}]", getName(), substring, text);
 		return response;
 	}
 
@@ -607,9 +611,10 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param regex The pattern to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertMatches(String regex) throws RestCallException {
+	public RestResponse assertMatches(String regex) throws RestCallException, AssertionError {
 		return assertMatches(regex, 0);
 	}
 
@@ -628,12 +633,13 @@ public class RestResponseHeader implements Header {
 	 * @param regex The pattern to test for.
 	 * @param flags Pattern match flags.  See {@link Pattern#compile(String, int)}.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertMatches(String regex, int flags) throws RestCallException {
+	public RestResponse assertMatches(String regex, int flags) throws RestCallException, AssertionError {
 		String text = asString();
 		if (! Pattern.compile(regex, flags).matcher(text).matches())
-			throw new RestCallException("Response did not match expected pattern in header {0}.\n\tpattern=[{1}]\n\tHeader=[{2}]", getName(), regex, text);
+			throw new BasicAssertionError("Response did not match expected pattern in header {0}.\n\tpattern=[{1}]\n\tHeader=[{2}]", getName(), regex, text);
 		return response;
 	}
 
@@ -655,12 +661,13 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param pattern The pattern to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws RestCallException If REST call failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertMatches(Pattern pattern) throws RestCallException {
+	public RestResponse assertMatches(Pattern pattern) throws RestCallException, AssertionError {
 		String text = asString();
 		if (! pattern.matcher(text).matches())
-			throw new RestCallException("Response did not match expected pattern in header {0}.\n\tpattern=[{1}]\n\tHeader=[{2}]", getName(), pattern.pattern(), text);
+			throw new BasicAssertionError("Response did not match expected pattern in header {0}.\n\tpattern=[{1}]\n\tHeader=[{2}]", getName(), pattern.pattern(), text);
 		return response;
 	}
 
diff --git a/juneau-rest/juneau-rest-server/pom.xml b/juneau-rest/juneau-rest-server/pom.xml
index dc03d7c..ecacb46 100644
--- a/juneau-rest/juneau-rest-server/pom.xml
+++ b/juneau-rest/juneau-rest-server/pom.xml
@@ -60,6 +60,10 @@
 			<groupId>com.sun.activation</groupId>
 			<artifactId>javax.activation</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpcore</artifactId>
+		</dependency>
 	</dependencies>
 
 	<properties>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
index 3c5a964..262e868 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
@@ -344,6 +344,9 @@ public class BasicRestCallHandler implements RestCallHandler {
 
 		call.exception(e);
 
+		if (call.isDebug())
+			e.printStackTrace();
+
 		int occurrence = context == null ? 0 : context.getStackTraceOccurrence(e);
 
 		int code = 500;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestFormData.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestFormData.java
index 3fef82e..949b7eb 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestFormData.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestFormData.java
@@ -16,6 +16,7 @@ import static org.apache.juneau.internal.ArrayUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
 
 import java.lang.reflect.*;
+import java.lang.reflect.Type;
 import java.util.*;
 
 import javax.servlet.http.*;
@@ -120,20 +121,12 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	/**
 	 * Returns a form-data parameter value.
 	 *
-	 * <p>
-	 * Parameter lookup is case-insensitive (consistent with WAS, but differs from Tomcat).
-	 *
 	 * <ul class='notes'>
 	 * 	<li>
+	 * 		Parameter lookup is case-insensitive (consistent with WAS, but differs from Tomcat).
+	 * 	<li>
 	 * 		This method returns the raw unparsed value, and differs from calling
-	 * 		<code>get(name, String.<jk>class</jk>)</code> which will convert the value from UON
-	 * 		notation:
-	 * 		<ul>
-	 * 			<li><js>"null"</js> =&gt; <jk>null</jk>
-	 * 			<li><js>"'null'"</js> =&gt; <js>"null"</js>
-	 * 			<li><js>"'foo bar'"</js> =&gt; <js>"foo bar"</js>
-	 * 			<li><js>"foo~~bar"</js> =&gt; <js>"foo~bar"</js>
-	 * 		</ul>
+	 * 		<code>get(name, String.<jk>class</jk>)</code> which uses the {@link HttpPartParser} for parsing the value.
 	 * </ul>
 	 *
 	 * @param name The form-data parameter name.
@@ -154,7 +147,15 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #getString(String)} except returns a default value if <jk>null</jk> or empty.
+	 * Returns a form-data parameter value.
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		Parameter lookup is case-insensitive (consistent with WAS, but differs from Tomcat).
+	 * 	<li>
+	 * 		This method returns the raw unparsed value, and differs from calling
+	 * 		<code>get(name, String.<jk>class</jk>)</code> which uses the {@link HttpPartParser} for parsing the value.
+	 * </ul>
 	 *
 	 * @param name The form-data parameter name.
 	 * @param def The default value.
@@ -166,7 +167,15 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #getString(String)} but converts the value to an integer.
+	 * Returns a form-data parameter value as an integer.
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		Parameter lookup is case-insensitive (consistent with WAS, but differs from Tomcat).
+	 * 	<li>
+	 * 		This method returns the raw unparsed value, and differs from calling
+	 * 		<code>get(name, Integer.<jk>class</jk>)</code> which uses the {@link HttpPartParser} for parsing the value.
+	 * </ul>
 	 *
 	 * @param name The form-data parameter name.
 	 * @return The parameter value, or <c>0</c> if parameter does not exist or is <jk>null</jk> or empty.
@@ -176,7 +185,15 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #getString(String,String)} but converts the value to an integer.
+	 * Returns a form-data parameter value as an integer.
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		Parameter lookup is case-insensitive (consistent with WAS, but differs from Tomcat).
+	 * 	<li>
+	 * 		This method returns the raw unparsed value, and differs from calling
+	 * 		<code>get(name, Integer.<jk>class</jk>)</code> which uses the {@link HttpPartParser} for parsing the value.
+	 * </ul>
 	 *
 	 * @param name The form-data parameter name.
 	 * @param def The default value.
@@ -188,7 +205,15 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #getString(String)} but converts the value to a boolean.
+	 * Returns a form-data parameter value as a boolean.
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		Parameter lookup is case-insensitive (consistent with WAS, but differs from Tomcat).
+	 * 	<li>
+	 * 		This method returns the raw unparsed value, and differs from calling
+	 * 		<code>get(name, Boolean.<jk>class</jk>)</code> which uses the {@link HttpPartParser} for parsing the value.
+	 * </ul>
 	 *
 	 * @param name The form-data parameter name.
 	 * @return The parameter value, or <jk>false</jk> if parameter does not exist or is <jk>null</jk> or empty.
@@ -198,7 +223,15 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #getString(String,String)} but converts the value to a boolean.
+	 * Returns a form-data parameter value as a boolean.
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		Parameter lookup is case-insensitive (consistent with WAS, but differs from Tomcat).
+	 * 	<li>
+	 * 		This method returns the raw unparsed value, and differs from calling
+	 * 		<code>get(name, Boolean.<jk>class</jk>)</code> which uses the {@link HttpPartParser} for parsing the value.
+	 * </ul>
 	 *
 	 * @param name The form-data parameter name.
 	 * @param def The default value.
@@ -246,7 +279,84 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #get(String, Object, Class)} but allows you to override the part parser.
+	 * Returns the specified form-data parameter value converted to a POJO using the {@link HttpPartParser} registered with the resource.
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Pipe-delimited list of comma-delimited numbers</jc>
+	 * 	HttpPartSchema schema = HttpPartSchema.<jsm>create</jsm>()
+	 * 		.items(
+	 * 			HttpPartSchema.<jsm>create</jsm>()
+	 * 			.collectionFormat(<js>"pipes"</js>)
+	 * 			.items(
+	 * 				HttpPartSchema.<jsm>create</jsm>()
+	 * 				.collectionFormat(<js>"csv"</js>)
+	 * 				.type(<js>"integer"</js>)
+	 * 				.format(<js>"int64"</js>)
+	 * 				.minimum(<js>"0"</js>)
+	 * 				.maximum(<js>"100"</js>)
+	 * 				.minLength(1)
+	 * 				.maxLength=(10)
+	 * 			)
+	 * 		)
+	 * 		.build();
+	 *
+	 * 	<jc>// Parse into a 2d long array.</jc>
+	 * 	<jk>long</jk>[][] myparams = formData.get(schema, <js>"myparam"</js>, <jk>long</jk>[][].<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
+	 *
+	 * @param schema
+	 * 	The schema object that defines the format of the input.
+	 * 	<br>If <jk>null</jk>, defaults to the schema defined on the parser.
+	 * 	<br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
+	 * 	<br>Only used if parser is schema-aware (e.g. {@link OpenApiParser}).
+	 * @param name The parameter name.
+	 * @param type The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws BadRequest Thrown if input could not be parsed.
+	 * @throws InternalServerError Thrown if any other exception occurs.
+	 */
+	public <T> T get(HttpPartSchema schema, String name, Class<T> type) throws BadRequest, InternalServerError {
+		return getInner(null, schema, name, null, getClassMeta(type));
+	}
+
+	/**
+	 * Returns the specified form-data parameter value converted to a POJO using the specified {@link HttpPartParser}.
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Pipe-delimited list of comma-delimited numbers</jc>
+	 * 	HttpPartSchema schema = HttpPartSchema.<jsm>create</jsm>()
+	 * 		.items(
+	 * 			HttpPartSchema.<jsm>create</jsm>()
+	 * 			.collectionFormat(<js>"pipes"</js>)
+	 * 			.items(
+	 * 				HttpPartSchema.<jsm>create</jsm>()
+	 * 				.collectionFormat(<js>"csv"</js>)
+	 * 				.type(<js>"integer"</js>)
+	 * 				.format(<js>"int64"</js>)
+	 * 				.minimum(<js>"0"</js>)
+	 * 				.maximum(<js>"100"</js>)
+	 * 				.minLength(1)
+	 * 				.maxLength=(10)
+	 * 			)
+	 * 		)
+	 * 		.build();
+	 *
+	 *  HttpPartParserSession parser = OpenApiParser.<jsf>DEFAULT</jsf>.createSession();
+	 *
+	 * 	<jc>// Parse into a 2d long array.</jc>
+	 * 	<jk>long</jk>[][] myparams = formData.get(parser, schema, <js>"myparam"</js>, <jk>long</jk>[][].<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
 	 *
 	 * @param parser
 	 * 	The parser to use for parsing the string value.
@@ -268,7 +378,29 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #get(String, Class)} except returns a default value if not specified.
+	 * Returns the specified form-data parameter value converted to a POJO using the {@link HttpPartParser} registered with the resource.
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Parse into an integer.</jc>
+	 * 	<jk>int</jk> myparam = formData.get(<js>"myparam"</js>, -1, <jk>int</jk>.<jk>class</jk>);
+	 *
+	 * 	<jc>// Parse into an int array.</jc>
+	 * 	<jk>int</jk>[] myparam = formData.get(<js>"myparam"</js>, <jk>new int</jk>[0], <jk>int</jk>[].<jk>class</jk>);
+
+	 * 	<jc>// Parse into a bean.</jc>
+	 * 	MyBean myparam = formData.get(<js>"myparam"</js>, <jk>new</jk> MyBean(), MyBean.<jk>class</jk>);
+	 *
+	 * 	<jc>// Parse into a linked-list of objects.</jc>
+	 * 	List myparam = formData.get(<js>"myparam"</js>, Collections.<jsm>emptyList</jsm>(), LinkedList.<jk>class</jk>);
+	 *
+	 * 	<jc>// Parse into a map of object keys/values.</jc>
+	 * 	Map myparam = formData.get(<js>"myparam"</js>, Collections.<jsm>emptyMap</jsm>(), TreeMap.<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
 	 *
 	 * @param name The parameter name.
 	 * @param def The default value if the parameter was not specified or is <jk>null</jk>.
@@ -283,11 +415,36 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #get(String, Object, Class)} but allows you to override the part parser.
+	 * Returns the specified form-data parameter value converted to a POJO using the {@link HttpPartParser} registered with the resource.
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Pipe-delimited list of comma-delimited numbers</jc>
+	 * 	HttpPartSchema schema = HttpPartSchema.<jsm>create</jsm>()
+	 * 		.items(
+	 * 			HttpPartSchema.<jsm>create</jsm>()
+	 * 			.collectionFormat(<js>"pipes"</js>)
+	 * 			.items(
+	 * 				HttpPartSchema.<jsm>create</jsm>()
+	 * 				.collectionFormat(<js>"csv"</js>)
+	 * 				.type(<js>"integer"</js>)
+	 * 				.format(<js>"int64"</js>)
+	 * 				.minimum(<js>"0"</js>)
+	 * 				.maximum(<js>"100"</js>)
+	 * 				.minLength(1)
+	 * 				.maxLength=(10)
+	 * 			)
+	 * 		)
+	 * 		.build();
+	 *
+	 * 	<jc>// Parse into a 2d long array.</jc>
+	 * 	<jk>long</jk>[][] myparams = formData.get(schema, <js>"myparam"</js>, <jk>new long</jk>[][0], <jk>long</jk>[][].<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
 	 *
-	 * @param parser
-	 * 	The parser to use for parsing the string value.
-	 * 	<br>If <jk>null</jk>, uses the part parser defined on the resource/method.
 	 * @param schema
 	 * 	The schema object that defines the format of the input.
 	 * 	<br>If <jk>null</jk>, defaults to the schema defined on the parser.
@@ -298,35 +455,45 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	 * @param type The class type to convert the parameter value to.
 	 * @param <T> The class type to convert the parameter value to.
 	 * @return The parameter value converted to the specified class type.
-	 * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
-	 * @throws InternalServerError Thrown if any other exception occurs.
-	 */
-	public <T> T get(HttpPartParserSession parser, HttpPartSchema schema, String name, T def, Class<T> type) throws BadRequest, InternalServerError {
-		return getInner(parser, schema, name, def, getClassMeta(type));
-	}
-
-	/**
-	 * Same as {@link #get(String, Class)} except for use on multi-part parameters
-	 * (e.g. <js>"key=1&amp;key=2&amp;key=3"</js> instead of <js>"key=@(1,2,3)"</js>)
-	 *
-	 * <p>
-	 * This method must only be called when parsing into classes of type Collection or array.
-	 *
-	 * @param name The parameter name.
-	 * @param type The class type to convert the parameter value to.
-	 * @return The parameter value converted to the specified class type.
 	 * @throws BadRequest Thrown if input could not be parsed.
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
-	public <T> T getAll(String name, Class<T> type) throws BadRequest, InternalServerError {
-		return getAllInner(null, null, name, null, getClassMeta(type));
+	public <T> T get(HttpPartSchema schema, String name, T def, Class<T> type) throws BadRequest, InternalServerError {
+		return getInner(null, schema, name, def, getClassMeta(type));
 	}
 
 	/**
-	 * Same as {@link #getAll(String, Class)} but allows you to override the part parser.
+	 * Returns the specified form-data parameter value converted to a POJO using the specified {@link HttpPartParser}.
 	 *
-	 * <p>
-	 * This method must only be called when parsing into classes of type Collection or array.
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Pipe-delimited list of comma-delimited numbers</jc>
+	 * 	HttpPartSchema schema = HttpPartSchema.<jsm>create</jsm>()
+	 * 		.items(
+	 * 			HttpPartSchema.<jsm>create</jsm>()
+	 * 			.collectionFormat(<js>"pipes"</js>)
+	 * 			.items(
+	 * 				HttpPartSchema.<jsm>create</jsm>()
+	 * 				.collectionFormat(<js>"csv"</js>)
+	 * 				.type(<js>"integer"</js>)
+	 * 				.format(<js>"int64"</js>)
+	 * 				.minimum(<js>"0"</js>)
+	 * 				.maximum(<js>"100"</js>)
+	 * 				.minLength(1)
+	 * 				.maxLength=(10)
+	 * 			)
+	 * 		)
+	 * 		.build();
+	 *
+	 *  HttpPartParserSession parser = OpenApiParser.<jsf>DEFAULT</jsf>.createSession();
+	 *
+	 * 	<jc>// Parse into a 2d long array.</jc>
+	 * 	<jk>long</jk>[][] myparams = formData.get(parser, schema, <js>"myparam"</js>, <jk>new long</jk>[][0], <jk>long</jk>[][].<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
 	 *
 	 * @param parser
 	 * 	The parser to use for parsing the string value.
@@ -337,13 +504,15 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	 * 	<br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
 	 * 	<br>Only used if parser is schema-aware (e.g. {@link OpenApiParser}).
 	 * @param name The parameter name.
+	 * @param def The default value if the parameter was not specified or is <jk>null</jk>.
 	 * @param type The class type to convert the parameter value to.
+	 * @param <T> The class type to convert the parameter value to.
 	 * @return The parameter value converted to the specified class type.
 	 * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
-	public <T> T getAll(HttpPartParserSession parser, HttpPartSchema schema, String name, Class<T> type) throws BadRequest, InternalServerError {
-		return getAllInner(parser, schema, name, null, getClassMeta(type));
+	public <T> T get(HttpPartParserSession parser, HttpPartSchema schema, String name, T def, Class<T> type) throws BadRequest, InternalServerError {
+		return getInner(parser, schema, name, def, getClassMeta(type));
 	}
 
 	/**
@@ -393,7 +562,7 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	public <T> T get(String name, Type type, Type...args) throws BadRequest, InternalServerError {
 		return getInner(null, null, name, null, this.<T>getClassMeta(type, args));
 	}
-
+	
 	/**
 	 * Same as {@link #get(String, Type, Type...)} but allows you to override the part parser.
 	 *
@@ -472,6 +641,148 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	}
 
 	/**
+	 * Returns the specified form-data parameter values converted POJO using the {@link HttpPartParser} registered with the resource.
+	 *
+	 * <p>
+	 * Meant to be used on multi-part parameters (e.g. <js>"key=1&amp;key=2&amp;key=3"</js> instead of <js>"key=@(1,2,3)"</js>)
+	 *
+	 * <p>
+	 * This method must only be called when parsing into classes of type Collection or array.
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Parse into multiple integers.</jc>
+	 * 	<jk>int</jk>[] myparam = formData.getAll(<js>"myparam"</js>, <jk>int</jk>[].<jk>class</jk>);
+	 *
+	 * 	<jc>// Parse into multiple int arrays.</jc>
+	 * 	<jk>int</jk>[][] myparam = formData.getAll(<js>"myparam"</js>, <jk>int</jk>[][].<jk>class</jk>);
+
+	 * 	<jc>// Parse into multiple beans.</jc>
+	 * 	MyBean[] myparam = formData.getAll(<js>"myparam"</js>, MyBean[].<jk>class</jk>);
+	 *
+	 * 	<jc>// Parse into multiple linked-lists of objects.</jc>
+	 * 	List[] myparam = formData.getAll(<js>"myparam"</js>, LinkedList[].<jk>class</jk>);
+	 *
+	 * 	<jc>// Parse into multiple maps of object keys/values.</jc>
+	 * 	Map[] myparam = formData.getAll(<js>"myparam"</js>, TreeMap[].<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
+	 *
+	 * @param name The parameter name.
+	 * @param type The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws BadRequest Thrown if input could not be parsed.
+	 * @throws InternalServerError Thrown if any other exception occurs.
+	 */
+	public <T> T getAll(String name, Class<T> type) throws BadRequest, InternalServerError {
+		return getAllInner(null, null, name, getClassMeta(type));
+	}
+
+	/**
+	 * Returns the specified form-data parameter values converted to POJOs using the {@link HttpPartParser} registered with the resource.
+	 *
+	 * <p>
+	 * Meant to be used on multi-part parameters (e.g. <js>"key=1&amp;key=2&amp;key=3"</js> instead of <js>"key=@(1,2,3)"</js>)
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Pipe-delimited list of comma-delimited numbers</jc>
+	 * 	HttpPartSchema schema = HttpPartSchema.<jsm>create</jsm>()
+	 * 		.items(
+	 * 			HttpPartSchema.<jsm>create</jsm>()
+	 * 			.collectionFormat(<js>"pipes"</js>)
+	 * 			.items(
+	 * 				HttpPartSchema.<jsm>create</jsm>()
+	 * 				.collectionFormat(<js>"csv"</js>)
+	 * 				.type(<js>"integer"</js>)
+	 * 				.format(<js>"int64"</js>)
+	 * 				.minimum(<js>"0"</js>)
+	 * 				.maximum(<js>"100"</js>)
+	 * 				.minLength(1)
+	 * 				.maxLength=(10)
+	 * 			)
+	 * 		)
+	 * 		.build();
+	 *
+	 * 	<jc>// Parse into multiple 2d long arrays.</jc>
+	 * 	<jk>long</jk>[][][] myparams = formData.getAll(schema, <js>"myparam"</js>, <jk>long</jk>[][][].<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
+	 *
+	 * @param schema
+	 * 	The schema object that defines the format of the input.
+	 * 	<br>If <jk>null</jk>, defaults to the schema defined on the parser.
+	 * 	<br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
+	 * 	<br>Only used if parser is schema-aware (e.g. {@link OpenApiParser}).
+	 * @param name The parameter name.
+	 * @param type The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws BadRequest Thrown if input could not be parsed.
+	 * @throws InternalServerError Thrown if any other exception occurs.
+	 */
+	public <T> T getAll(HttpPartSchema schema, String name, Class<T> type) throws BadRequest, InternalServerError {
+		return getAllInner(null, schema, name, getClassMeta(type));
+	}
+
+	/**
+	 * Returns the specified form-data parameter values converted to POJOs using the specified {@link HttpPartParser}.
+	 *
+	 * <p>
+	 * Meant to be used on multi-part parameters (e.g. <js>"key=1&amp;key=2&amp;key=3"</js> instead of <js>"key=@(1,2,3)"</js>)
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Pipe-delimited list of comma-delimited numbers</jc>
+	 * 	HttpPartSchema schema = HttpPartSchema.<jsm>create</jsm>()
+	 * 		.items(
+	 * 			HttpPartSchema.<jsm>create</jsm>()
+	 * 			.collectionFormat(<js>"pipes"</js>)
+	 * 			.items(
+	 * 				HttpPartSchema.<jsm>create</jsm>()
+	 * 				.collectionFormat(<js>"csv"</js>)
+	 * 				.type(<js>"integer"</js>)
+	 * 				.format(<js>"int64"</js>)
+	 * 				.minimum(<js>"0"</js>)
+	 * 				.maximum(<js>"100"</js>)
+	 * 				.minLength(1)
+	 * 				.maxLength=(10)
+	 * 			)
+	 * 		)
+	 * 		.build();
+	 *
+	 * 	<jc>// Parse into multiple 2d long arrays.</jc>
+	 * 	<jk>long</jk>[][][] myparams = formData.getAll(schema, <js>"myparam"</js>, <jk>long</jk>[][][].<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
+	 *
+	 * @param parser
+	 * 	The parser to use for parsing the string value.
+	 * 	<br>If <jk>null</jk>, uses the part parser defined on the resource/method.
+	 * @param schema
+	 * 	The schema object that defines the format of the input.
+	 * 	<br>If <jk>null</jk>, defaults to the schema defined on the parser.
+	 * 	<br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
+	 * 	<br>Only used if parser is schema-aware (e.g. {@link OpenApiParser}).
+	 * @param name The parameter name.
+	 * @param type The class type to convert the parameter value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
+	 * @throws InternalServerError Thrown if any other exception occurs.
+	 */
+	public <T> T getAll(HttpPartParserSession parser, HttpPartSchema schema, String name, Class<T> type) throws BadRequest, InternalServerError {
+		return getAllInner(parser, schema, name, getClassMeta(type));
+	}
+
+	/**
 	 * Same as {@link #get(String, Type, Type...)} except for use on multi-part parameters
 	 * (e.g. <js>"key=1&amp;key=2&amp;key=3"</js> instead of <js>"key=@(1,2,3)"</js>)
 	 *
@@ -491,7 +802,7 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
 	public <T> T getAll(String name, Type type, Type...args) throws BadRequest, InternalServerError {
-		return getAllInner(null, null, name, null, this.<T>getClassMeta(type, args));
+		return getAllInner(null, null, name, this.<T>getClassMeta(type, args));
 	}
 
 	/**
@@ -518,11 +829,13 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
 	public <T> T getAll(HttpPartParserSession parser, HttpPartSchema schema, String name, Type type, Type...args) throws BadRequest, InternalServerError {
-		return getAllInner(parser, schema, name, null, this.<T>getClassMeta(type, args));
+		return getAllInner(parser, schema, name, this.<T>getClassMeta(type, args));
 	}
 
 	/* Workhorse method */
 	private <T> T getInner(HttpPartParserSession parser, HttpPartSchema schema, String name, T def, ClassMeta<T> cm) throws BadRequest, InternalServerError {
+		if (parser == null)
+			parser = req.getPartParser();
 		try {
 			if (cm.isMapOrBean() && isOneOf(name, "*", "")) {
 				OMap m = new OMap();
@@ -531,7 +844,7 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 					HttpPartSchema pschema = schema == null ? null : schema.getProperty(k);
 					ClassMeta<?> cm2 = cm.getValueType();
 					if (cm.getValueType().isCollectionOrArray())
-						m.put(k, getAllInner(parser, pschema, k, null, cm2));
+						m.put(k, getAllInner(parser, pschema, k, cm2));
 					else
 						m.put(k, getInner(parser, pschema, k, null, cm2));
 				}
@@ -550,10 +863,8 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 
 	/* Workhorse method */
 	@SuppressWarnings("rawtypes")
-	<T> T getAllInner(HttpPartParserSession parser, HttpPartSchema schema, String name, T def, ClassMeta<T> cm) throws BadRequest, InternalServerError {
+	<T> T getAllInner(HttpPartParserSession parser, HttpPartSchema schema, String name, ClassMeta<T> cm) throws BadRequest, InternalServerError {
 		String[] p = get(name);
-		if (p == null)
-			return def;
 		if (schema == null)
 			schema = HttpPartSchema.DEFAULT;
 		try {
@@ -575,7 +886,7 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 		} catch (Exception e) {
 			throw new InternalServerError(e, "Could not parse form-data parameter ''{0}''.", name) ;
 		}
-		throw new InternalServerError("Invalid call to getParameters(String, ClassMeta).  Class type must be a Collection or array.");
+		throw new InternalServerError("Invalid call to getAll(String, ClassMeta).  Class type must be a Collection or array.");
 	}
 
 	private <T> T parse(HttpPartParserSession parser, HttpPartSchema schema, String val, ClassMeta<T> c) throws SchemaValidationException, ParseException {
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseHeader.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeader.java
similarity index 82%
copy from juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseHeader.java
copy to juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeader.java
index 0023f46..6f76f60 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponseHeader.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeader.java
@@ -10,7 +10,7 @@
 // * "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.client2;
+package org.apache.juneau.rest;
 
 import static org.apache.juneau.httppart.HttpPartType.*;
 import java.lang.reflect.*;
@@ -28,17 +28,8 @@ import org.apache.juneau.utils.*;
 
 /**
  * Represents a single header on an HTTP response.
- *
- * <p>
- * An extension of an HttpClient {@link Header} that provides various support for converting the header to POJOs and
- * other convenience methods.
- *
- * <ul class='seealso'>
- * 	<li class='jc'>{@link RestClient}
- * 	<li class='link'>{@doc juneau-rest-client}
- * </ul>
  */
-public class RestResponseHeader implements Header {
+public class RequestHeader implements Header {
 
 	static final Header NULL_HEADER = new Header() {
 
@@ -71,7 +62,7 @@ public class RestResponseHeader implements Header {
 	 * @param response The response object.
 	 * @param header The wrapped header.  Can be <jk>null</jk>.
 	 */
-	public RestResponseHeader(RestRequest request, RestResponse response, Header header) {
+	public RequestHeader(RestRequest request, RestResponse response, Header header) {
 		this.request = request;
 		this.response = response;
 		this.header = header == null ? NULL_HEADER : header;
@@ -92,7 +83,7 @@ public class RestResponseHeader implements Header {
 	 * 	The part schema.
 	 * @return This object (for method chaining).
 	 */
-	public RestResponseHeader schema(HttpPartSchema value) {
+	public RequestHeader schema(HttpPartSchema value) {
 		this.schema = value;
 		return this;
 	}
@@ -101,14 +92,14 @@ public class RestResponseHeader implements Header {
 	 * Specifies the part parser to use for this header.
 	 *
 	 * <p>
-	 * If not specified, uses the part parser defined on the client by calling {@link RestClientBuilder#partParser(Class)}.
+	 * If not specified, uses the part parser defined on the client by calling {@link RestContextBuilder#partParser(Class)}.
 	 *
 	 * @param value
 	 * 	The new part parser to use for this header.
 	 * 	<br>If <jk>null</jk>, {@link SimplePartParser#DEFAULT} will be used.
 	 * @return This object (for method chaining).
 	 */
-	public RestResponseHeader parser(HttpPartParserSession value) {
+	public RequestHeader parser(HttpPartParserSession value) {
 		this.parser = value == null ? SimplePartParser.DEFAULT_SESSION : value;
 		return this;
 	}
@@ -184,9 +175,9 @@ public class RestResponseHeader implements Header {
 	 * @param type The type to convert to.
 	 * @param args The type parameters.
 	 * @return The converted type, or <jk>null</jk> if header is not present.
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> T as(Type type, Type...args) throws RestCallException {
+	public <T> T as(Type type, Type...args) throws ParseException {
 		return as(request.getClassMeta(type, args));
 	}
 
@@ -198,9 +189,9 @@ public class RestResponseHeader implements Header {
 	 * @param type The type to convert to.
 	 * @param args The type parameters.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> RestResponse as(Mutable<T> m, Type type, Type...args) throws RestCallException {
+	public <T> RestResponse as(Mutable<T> m, Type type, Type...args) throws ParseException {
 		m.set(as(type, args));
 		return response;
 	}
@@ -211,9 +202,9 @@ public class RestResponseHeader implements Header {
 	 * @param <T> The type to convert to.
 	 * @param type The type to convert to.
 	 * @return The converted type, or <jk>null</jk> if header is not present.
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> T as(Class<T> type) throws RestCallException {
+	public <T> T as(Class<T> type) throws ParseException {
 		return as(request.getClassMeta(type));
 	}
 
@@ -224,9 +215,9 @@ public class RestResponseHeader implements Header {
 	 * @param <T> The type to convert to.
 	 * @param type The type to convert to.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> RestResponse as(Mutable<T> m, Class<T> type) throws RestCallException {
+	public <T> RestResponse as(Mutable<T> m, Class<T> type) throws ParseException {
 		m.set(as(type));
 		return response;
 	}
@@ -237,14 +228,10 @@ public class RestResponseHeader implements Header {
 	 * @param <T> The type to convert to.
 	 * @param type The type to convert to.
 	 * @return The converted type, or <jk>null</jk> if header is not present.
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> T as(ClassMeta<T> type) throws RestCallException {
-		try {
-			return parser.parse(HEADER, schema, asString(), type);
-		} catch (ParseException e) {
-			throw new RestCallException(e);
-		}
+	public <T> T as(ClassMeta<T> type) throws ParseException {
+		return parser.parse(HEADER, schema, asString(), type);
 	}
 
 	/**
@@ -254,9 +241,9 @@ public class RestResponseHeader implements Header {
 	 * @param <T> The type to convert to.
 	 * @param type The type to convert to.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> RestResponse as(Mutable<T> m, ClassMeta<T> type) throws RestCallException {
+	public <T> RestResponse as(Mutable<T> m, ClassMeta<T> type) throws ParseException {
 		m.set(as(type));
 		return response;
 	}
@@ -268,9 +255,9 @@ public class RestResponseHeader implements Header {
 	 * @param type The type to convert to.
 	 * @param args The type parameters.
 	 * @return The parsed value as an {@link Optional}, or an empty optional if header was not present.
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> Optional<T> asOptional(Type type, Type...args) throws RestCallException {
+	public <T> Optional<T> asOptional(Type type, Type...args) throws ParseException {
 		return Optional.ofNullable(as(type, args));
 	}
 
@@ -282,9 +269,9 @@ public class RestResponseHeader implements Header {
 	 * @param type The type to convert to.
 	 * @param args The type parameters.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> RestResponse asOptional(Mutable<Optional<T>> m, Type type, Type...args) throws RestCallException {
+	public <T> RestResponse asOptional(Mutable<Optional<T>> m, Type type, Type...args) throws ParseException {
 		m.set(asOptional(type, args));
 		return response;
 	}
@@ -295,9 +282,9 @@ public class RestResponseHeader implements Header {
 	 * @param <T> The type to convert to.
 	 * @param type The type to convert to.
 	 * @return The parsed value as an {@link Optional}, or an empty optional if header was not present.
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> Optional<T> asOptional(Class<T> type) throws RestCallException {
+	public <T> Optional<T> asOptional(Class<T> type) throws ParseException {
 		return Optional.ofNullable(as(type));
 	}
 
@@ -308,9 +295,9 @@ public class RestResponseHeader implements Header {
 	 * @param <T> The type to convert to.
 	 * @param type The type to convert to.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> RestResponse asOptional(Mutable<Optional<T>> m, Class<T> type) throws RestCallException {
+	public <T> RestResponse asOptional(Mutable<Optional<T>> m, Class<T> type) throws ParseException {
 		m.set(asOptional(type));
 		return response;
 	}
@@ -321,9 +308,9 @@ public class RestResponseHeader implements Header {
 	 * @param <T> The type to convert to.
 	 * @param type The type to convert to.
 	 * @return The parsed value as an {@link Optional}, or an empty optional if header was not present.
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> Optional<T> asOptional(ClassMeta<T> type) throws RestCallException {
+	public <T> Optional<T> asOptional(ClassMeta<T> type) throws ParseException {
 		return Optional.ofNullable(as(type));
 	}
 
@@ -334,9 +321,9 @@ public class RestResponseHeader implements Header {
 	 * @param <T> The type to convert to.
 	 * @param type The type to convert to.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If value could not be parsed.
+	 * @throws ParseException If value could not be parsed.
 	 */
-	public <T> RestResponse asOptional(Mutable<Optional<T>> m, ClassMeta<T> type) throws RestCallException {
+	public <T> RestResponse asOptional(Mutable<Optional<T>> m, ClassMeta<T> type) throws ParseException {
 		m.set(asOptional(type));
 		return response;
 	}
@@ -358,9 +345,8 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param pattern The regular expression pattern to match.
 	 * @return The matcher.
-	 * @throws RestCallException If a connection error occurred.
 	 */
-	public Matcher asMatcher(Pattern pattern) throws RestCallException {
+	public Matcher asMatcher(Pattern pattern) {
 		return pattern.matcher(asString());
 	}
 
@@ -383,9 +369,8 @@ public class RestResponseHeader implements Header {
 	 * @param m The mutable to set the value in.
 	 * @param pattern The regular expression pattern to match.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If a connection error occurred.
 	 */
-	public RestResponse asMatcher(Mutable<Matcher> m, Pattern pattern) throws RestCallException {
+	public RestResponse asMatcher(Mutable<Matcher> m, Pattern pattern) {
 		m.set(pattern.matcher(asString()));
 		return response;
 	}
@@ -407,9 +392,8 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param regex The regular expression pattern to match.
 	 * @return The matcher.
-	 * @throws RestCallException If a connection error occurred.
 	 */
-	public Matcher asMatcher(String regex) throws RestCallException {
+	public Matcher asMatcher(String regex) {
 		return asMatcher(regex, 0);
 	}
 
@@ -432,9 +416,8 @@ public class RestResponseHeader implements Header {
 	 * @param m The mutable to set the value in.
 	 * @param regex The regular expression pattern to match.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If a connection error occurred.
 	 */
-	public RestResponse asMatcher(Mutable<Matcher> m, String regex) throws RestCallException {
+	public RestResponse asMatcher(Mutable<Matcher> m, String regex) {
 		asMatcher(regex, 0);
 		return response;
 	}
@@ -457,9 +440,8 @@ public class RestResponseHeader implements Header {
 	 * @param regex The regular expression pattern to match.
 	 * @param flags Pattern match flags.  See {@link Pattern#compile(String, int)}.
 	 * @return The matcher.
-	 * @throws RestCallException If a connection error occurred.
 	 */
-	public Matcher asMatcher(String regex, int flags) throws RestCallException {
+	public Matcher asMatcher(String regex, int flags) {
 		return asMatcher(Pattern.compile(regex, flags));
 	}
 
@@ -483,9 +465,8 @@ public class RestResponseHeader implements Header {
 	 * @param regex The regular expression pattern to match.
 	 * @param flags Pattern match flags.  See {@link Pattern#compile(String, int)}.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If a connection error occurred.
 	 */
-	public RestResponse asMatcher(Mutable<Matcher> m, String regex, int flags) throws RestCallException {
+	public RestResponse asMatcher(Mutable<Matcher> m, String regex, int flags) {
 		asMatcher(Pattern.compile(regex, flags));
 		return response;
 	}
@@ -516,11 +497,11 @@ public class RestResponseHeader implements Header {
 	 * </p>
 	 *
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertExists() throws RestCallException {
+	public RestResponse assertExists() throws AssertionError {
 		if (! exists())
-			throw new RestCallException("Response did not have the expected header {0}.", getName());
+			throw new BasicAssertionError("Response did not have the expected header {0}.", getName());
 		return response;
 	}
 
@@ -538,11 +519,11 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param value The value to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertValue(String value) throws RestCallException {
+	public RestResponse assertValue(String value) throws AssertionError {
 		if (! StringUtils.isEquals(value, asString()))
-			throw new RestCallException("Response did not have the expected value for header {0}.\n\tExpected=[{1}]\n\tActual=[{2}]", getName(), value, asString());
+			throw new BasicAssertionError("Response did not have the expected value for header {0}.\n\tExpected=[{1}]\n\tActual=[{2}]", getName(), value, asString());
 		return response;
 	}
 
@@ -560,12 +541,12 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param test The predicate to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertValue(Predicate<String> test) throws RestCallException {
+	public RestResponse assertValue(Predicate<String> test) throws AssertionError {
 		String text = asString();
 		if (! test.test(text))
-			throw new RestCallException("Response did not have the expected value for header {0}.\n\tActual=[{1}]", getName(), text);
+			throw new BasicAssertionError("Response did not have the expected value for header {0}.\n\tActual=[{1}]", getName(), text);
 		return response;
 	}
 
@@ -583,13 +564,13 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param values The substrings to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertContains(String...values) throws RestCallException {
+	public RestResponse assertContains(String...values) throws AssertionError {
 		String text = asString();
 		for (String substring : values)
 			if (! StringUtils.contains(text, substring))
-				throw new RestCallException("Response did not have the expected substring in header {0}.\n\tExpected=[{1}]\n\tHeader=[{2}]", getName(), substring, text);
+				throw new BasicAssertionError("Response did not have the expected substring in header {0}.\n\tExpected=[{1}]\n\tHeader=[{2}]", getName(), substring, text);
 		return response;
 	}
 
@@ -607,9 +588,9 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param regex The pattern to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertMatches(String regex) throws RestCallException {
+	public RestResponse assertMatches(String regex) throws AssertionError {
 		return assertMatches(regex, 0);
 	}
 
@@ -628,12 +609,12 @@ public class RestResponseHeader implements Header {
 	 * @param regex The pattern to test for.
 	 * @param flags Pattern match flags.  See {@link Pattern#compile(String, int)}.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertMatches(String regex, int flags) throws RestCallException {
+	public RestResponse assertMatches(String regex, int flags) throws AssertionError {
 		String text = asString();
 		if (! Pattern.compile(regex, flags).matcher(text).matches())
-			throw new RestCallException("Response did not match expected pattern in header {0}.\n\tpattern=[{1}]\n\tHeader=[{2}]", getName(), regex, text);
+			throw new BasicAssertionError("Response did not match expected pattern in header {0}.\n\tpattern=[{1}]\n\tHeader=[{2}]", getName(), regex, text);
 		return response;
 	}
 
@@ -655,12 +636,12 @@ public class RestResponseHeader implements Header {
 	 *
 	 * @param pattern The pattern to test for.
 	 * @return The response object (for method chaining).
-	 * @throws RestCallException If assertion failed.
+	 * @throws AssertionError If assertion failed.
 	 */
-	public RestResponse assertMatches(Pattern pattern) throws RestCallException {
+	public RestResponse assertMatches(Pattern pattern) throws AssertionError {
 		String text = asString();
 		if (! pattern.matcher(text).matches())
-			throw new RestCallException("Response did not match expected pattern in header {0}.\n\tpattern=[{1}]\n\tHeader=[{2}]", getName(), pattern.pattern(), text);
+			throw new BasicAssertionError("Response did not match expected pattern in header {0}.\n\tpattern=[{1}]\n\tHeader=[{2}]", getName(), pattern.pattern(), text);
 		return response;
 	}
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeaders.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeaders.java
index 68e8577..da94030 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeaders.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeaders.java
@@ -165,17 +165,27 @@ public class RequestHeaders extends TreeMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #getString(String)} but converts the value to an integer.
+	 * Returns the specified header value as an integer.
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks for {@code &HeaderName=x} in the URL query string.
+	 * </ul>
 	 *
 	 * @param name The HTTP header name.
-	 * @return The header value, or the default value if the header isn't present.
+	 * @return The header value, or <code>0</code> value if the header isn't present.
 	 */
 	public int getInt(String name) {
 		return getInt(name, 0);
 	}
 
 	/**
-	 * Same as {@link #getString(String,String)} but converts the value to an integer.
+	 * Returns the specified header value as an integer.
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks for {@code &HeaderName=x} in the URL query string.
+	 * </ul>
 	 *
 	 * @param name The HTTP header name.
 	 * @param def The default value to return if the header value isn't found.
@@ -187,7 +197,12 @@ public class RequestHeaders extends TreeMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #getString(String)} but converts the value to a boolean.
+	 * Returns the specified header value as a boolean.
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks for {@code &HeaderName=x} in the URL query string.
+	 * </ul>
 	 *
 	 * @param name The HTTP header name.
 	 * @return The header value, or the default value if the header isn't present.
@@ -197,7 +212,12 @@ public class RequestHeaders extends TreeMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #getString(String,String)} but converts the value to a boolean.
+	 * Returns the specified header value as a boolean.
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks for {@code &HeaderName=x} in the URL query string.
+	 * </ul>
 	 *
 	 * @param name The HTTP header name.
 	 * @param def The default value to return if the header value isn't found.
@@ -227,10 +247,10 @@ public class RequestHeaders extends TreeMap<String,String[]> {
 	 * <h5 class='section'>Examples:</h5>
 	 * <p class='bcode w800'>
 	 * 	<jc>// Parse into an integer.</jc>
-	 * 	<jk>int</jk> myheader = req.getHeader(<js>"My-Header"</js>, <jk>int</jk>.<jk>class</jk>);
+	 * 	<jk>int</jk> myheader = req.getHeaders().get(<js>"My-Header"</js>, <jk>int</jk>.<jk>class</jk>);
 	 *
 	 * 	<jc>// Parse a UUID.</jc>
-	 * 	UUID myheader = req.getHeader(<js>"My-Header"</js>, UUID.<jk>class</jk>);
+	 * 	UUID myheader = req.getHeaders().get(<js>"My-Header"</js>, UUID.<jk>class</jk>);
 	 * </p>
 	 *
 	 * <ul class='notes'>
@@ -254,7 +274,59 @@ public class RequestHeaders extends TreeMap<String,String[]> {
 	}
 
 	/**
-	 * Same as {@link #get(String, Class)} but allows you to override the part parser used.
+	 * Returns all headers with the specified name converted to a POJO using the {@link HttpPartParser} registered with the resource.
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Parse into an integer.</jc>
+	 * 	<jk>int</jk>[] myheaders = req.getHeaders().getAll(<js>"My-Header"</js>, <jk>int</jk>[].<jk>class</jk>);
+	 *
+	 * 	<jc>// Parse a UUID.</jc>
+	 * 	UUID[] myheaders = req.getHeaders().getAll(<js>"My-Header"</js>, UUID[].<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks for {@code &HeaderName=x} in the URL query string.
+	 * 	<li>
+	 * 		The class must be an array or collection type.
+	 * </ul>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
+	 *
+	 * @param name The HTTP header name.
+	 * @param type The class type to convert the header value to.
+	 * @param <T> The class type to convert the header value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws BadRequest Thrown if input could not be parsed.
+	 * @throws InternalServerError Thrown if any other exception occurs.
+	 */
+	public <T> T getAll(String name, Class<T> type) throws BadRequest, InternalServerError {
+		return getAllInner(null, null, name, getClassMeta(type));
+	}
+
+	/**
+	 * Returns the specified header value converted to a POJO using the specified part parser.
+	 *
+	 * <h5 class='section'>Examples:</h5>
+	 * <p class='bcode w800'>
+	 * 	<jc>// Parse into an integer.</jc>
+	 * 	<jk>int</jk> myheader = req.getHeaders().get(<js>"My-Header"</js>, <jk>int</jk>.<jk>class</jk>);
+	 *
+	 * 	<jc>// Parse a UUID.</jc>
+	 * 	UUID myheader = req.getHeaders().get(<js>"My-Header"</js>, UUID.<jk>class</jk>);
+	 * </p>
+	 *
+	 * <ul class='notes'>
+	 * 	<li>
+	 * 		If {@code allowHeaderParams} init parameter is <jk>true</jk>, then first looks for {@code &HeaderName=x} in the URL query string.
+	 * </ul>
+	 *
+	 * <ul class='seealso'>
+	 * 	<li class='jf'>{@link RestContext#REST_partParser}
+	 * </ul>
 	 *
 	 * @param parser
 	 * 	The parser to use for parsing the string header.
@@ -384,8 +456,39 @@ public class RequestHeaders extends TreeMap<String,String[]> {
 		return getInner(parser, schema, name, null, this.<T>getClassMeta(type, args));
 	}
 
+	/**
+	 * Converts all the headers with the specified name to the specified type.
+	 *
+	 * @param parser
+	 * 	The parser to use for parsing the string header.
+	 * 	<br>If <jk>null</jk>, uses the part parser defined on the resource/method.
+	 * @param schema
+	 * 	The schema object that defines the format of the input.
+	 * 	<br>If <jk>null</jk>, defaults to the schema defined on the parser.
+	 * 	<br>If that's also <jk>null</jk>, defaults to {@link HttpPartSchema#DEFAULT}.
+	 * 	<br>Only used if parser is schema-aware (e.g. {@link OpenApiParser}).
+	 * @param name
+	 * 	The HTTP header name.
+	 * @param type
+	 * 	The type of object to create.
+	 * 	<br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
+	 * @param args
+	 * 	The type arguments of the class if it's a collection or map.
+	 * 	<br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
+	 * 	<br>Ignored if the main type is not a map or collection.
+	 * @param <T> The class type to convert the header value to.
+	 * @return The parameter value converted to the specified class type.
+	 * @throws BadRequest Thrown if input could not be parsed or fails schema validation.
+	 * @throws InternalServerError Thrown if any other exception occurs.
+	 */
+	public <T> T getAll(HttpPartParserSession parser, HttpPartSchema schema, String name, Type type, Type...args) throws BadRequest, InternalServerError {
+		return getAllInner(parser, schema, name, this.<T>getClassMeta(type, args));
+	}
+
 	/* Workhorse method */
 	private <T> T getInner(HttpPartParserSession parser, HttpPartSchema schema, String name, T def, ClassMeta<T> cm) throws BadRequest, InternalServerError {
+		if (parser == null)
+			parser = req.getPartParser();
 		try {
 			if (cm.isMapOrBean() && isOneOf(name, "*", "")) {
 				OMap m = new OMap();
@@ -409,6 +512,34 @@ public class RequestHeaders extends TreeMap<String,String[]> {
 	}
 
 	/* Workhorse method */
+	@SuppressWarnings({ "rawtypes", "unchecked" })
+	<T> T getAllInner(HttpPartParserSession parser, HttpPartSchema schema, String name, ClassMeta<T> cm) throws BadRequest, InternalServerError {
+		String[] p = get(name);
+		if (schema == null)
+			schema = HttpPartSchema.DEFAULT;
+		try {
+			if (cm.isArray()) {
+				List c = new ArrayList();
+				for (int i = 0; i < p.length; i++)
+					c.add(parse(parser, schema.getItems(), p[i], cm.getElementType()));
+				return (T)toArray(c, cm.getElementType().getInnerClass());
+			} else if (cm.isCollection()) {
+				Collection c = (Collection)(cm.canCreateNewInstance() ? cm.newInstance() : new OList());
+				for (int i = 0; i < p.length; i++)
+					c.add(parse(parser, schema.getItems(), p[i], cm.getElementType()));
+				return (T)c;
+			}
+		} catch (SchemaValidationException e) {
+			throw new BadRequest(e, "Validation failed on header ''{0}''. ", name);
+		} catch (ParseException e) {
+			throw new BadRequest(e, "Could not parse header ''{0}''.", name) ;
+		} catch (Exception e) {
+			throw new InternalServerError(e, "Could not parse header ''{0}''.", name);
+		}
+		throw new InternalServerError("Invalid call to getAll(String, ClassMeta).  Class type must be a Collection or array.");
+	}
+
+	/* Workhorse method */
 	private <T> T parse(HttpPartParserSession parser, HttpPartSchema schema, String val, ClassMeta<T> cm) throws SchemaValidationException, ParseException {
 		if (parser == null)
 			parser = this.parser;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestPath.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestPath.java
index 1ce2c9d..d2dae59 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestPath.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestPath.java
@@ -147,7 +147,7 @@ public class RequestPath extends TreeMap<String,String> {
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
 	public <T> T get(String name, Class<T> type) throws BadRequest, InternalServerError {
-		return getInner(null, null, name, null, this.<T>getClassMeta(type));
+		return getInner(null, null, name, null, getClassMeta(type));
 	}
 
 	/**
@@ -169,7 +169,7 @@ public class RequestPath extends TreeMap<String,String> {
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
 	public <T> T get(HttpPartParserSession parser, HttpPartSchema schema, String name, Class<T> type) throws BadRequest, InternalServerError {
-		return getInner(parser, schema, name, null, this.<T>getClassMeta(type));
+		return getInner(parser, schema, name, null, getClassMeta(type));
 	}
 
 	/**
@@ -221,7 +221,7 @@ public class RequestPath extends TreeMap<String,String> {
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
 	public <T> T get(String name, Type type, Type...args) throws BadRequest, InternalServerError {
-		return getInner(null, null, name, null, this.<T>getClassMeta(type, args));
+		return getInner(null, null, name, null, getClassMeta(type, args));
 	}
 
 	/**
@@ -249,11 +249,13 @@ public class RequestPath extends TreeMap<String,String> {
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
 	public <T> T get(HttpPartParserSession parser, HttpPartSchema schema, String name, Type type, Type...args) throws BadRequest, InternalServerError {
-		return getInner(parser, schema, name, null, this.<T>getClassMeta(type, args));
+		return getInner(parser, schema, name, null, getClassMeta(type, args));
 	}
 
 	/* Workhorse method */
 	private <T> T getInner(HttpPartParserSession parser, HttpPartSchema schema, String name, T def, ClassMeta<T> cm) throws BadRequest, InternalServerError {
+		if (parser == null)
+			parser = req.getPartParser();
 		try {
 			if (cm.isMapOrBean() && isOneOf(name, "*", "")) {
 				OMap m = new OMap();
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestQuery.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestQuery.java
index 5ade3a8..c8dfd04 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestQuery.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestQuery.java
@@ -482,7 +482,7 @@ public final class RequestQuery extends LinkedHashMap<String,String[]> {
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
 	public <T> T getAll(String name, Class<T> c) throws BadRequest, InternalServerError {
-		return getAllInner(null, null, name, null, getClassMeta(c));
+		return getAllInner(null, null, name, getClassMeta(c));
 	}
 
 	/**
@@ -506,7 +506,7 @@ public final class RequestQuery extends LinkedHashMap<String,String[]> {
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
 	public <T> T getAll(String name, Type type, Type...args) throws BadRequest, InternalServerError {
-		return getAllInner(null, null, name, null, (ClassMeta<T>)getClassMeta(type, args));
+		return getAllInner(null, null, name, (ClassMeta<T>)getClassMeta(type, args));
 	}
 
 	/**
@@ -534,7 +534,7 @@ public final class RequestQuery extends LinkedHashMap<String,String[]> {
 	 * @throws InternalServerError Thrown if any other exception occurs.
 	 */
 	public <T> T getAll(HttpPartParserSession parser, HttpPartSchema schema, String name, Type type, Type...args) throws BadRequest, InternalServerError {
-		return getAllInner(parser, schema, name, null, (ClassMeta<T>)getClassMeta(type, args));
+		return getAllInner(parser, schema, name, getClassMeta(type, args));
 	}
 
 	/**
@@ -616,6 +616,8 @@ public final class RequestQuery extends LinkedHashMap<String,String[]> {
 
 	/* Workhorse method */
 	private <T> T getInner(HttpPartParserSession parser, HttpPartSchema schema, String name, T def, ClassMeta<T> cm) throws BadRequest, InternalServerError {
+		if (parser == null)
+			parser = req.getPartParser();
 		try {
 			if (cm.isMapOrBean() && isOneOf(name, "*", "")) {
 				OMap m = new OMap();
@@ -624,7 +626,7 @@ public final class RequestQuery extends LinkedHashMap<String,String[]> {
 					HttpPartSchema pschema = schema == null ? null : schema.getProperty(k);
 					ClassMeta<?> cm2 = cm.getValueType();
 					if (cm.getValueType().isCollectionOrArray())
-						m.put(k, getAllInner(parser, pschema, k, null, cm2));
+						m.put(k, getAllInner(parser, pschema, k, cm2));
 					else
 						m.put(k, getInner(parser, pschema, k, null, cm2));
 				}
@@ -643,10 +645,8 @@ public final class RequestQuery extends LinkedHashMap<String,String[]> {
 
 	/* Workhorse method */
 	@SuppressWarnings("rawtypes")
-	private <T> T getAllInner(HttpPartParserSession parser, HttpPartSchema schema, String name, T def, ClassMeta<T> cm) throws BadRequest, InternalServerError {
+	private <T> T getAllInner(HttpPartParserSession parser, HttpPartSchema schema, String name, ClassMeta<T> cm) throws BadRequest, InternalServerError {
 		String[] p = get(name);
-		if (p == null)
-			return def;
 		if (schema == null)
 			schema = HttpPartSchema.DEFAULT;
 		try {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java
index f897a42..3aed6db 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCall.java
@@ -366,4 +366,15 @@ public class RestCall {
 			return rres.getOutput();
 		return null;
 	}
+
+	/**
+	 * Shortcut for calling <c>getRestRequest().isDebug()</c>.
+	 *
+	 * @return <jk>true</jk> if debug is enabled for this request.
+	 */
+	public boolean isDebug() {
+		if (rreq != null)
+			return rreq.isDebug();
+		return false;
+	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 2e43461..cd4bcc4 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -3816,22 +3816,8 @@ public final class RestContext extends BeanContext {
 				.create()
 				.append(getInstanceArrayProperty(REST_parsers, Parser.class, new Parser[0], resourceResolver, resource, ps))
 				.build();
-			partSerializer =
-				(HttpPartSerializer)
-				SerializerGroup
-				.create()
-				.append(getInstanceProperty(REST_partSerializer, HttpPartSerializer.class, OpenApiSerializer.class, resourceResolver, resource, ps))
-				.build()
-				.getSerializers()
-				.get(0);
-			partParser =
-				(HttpPartParser)
-				ParserGroup
-				.create()
-				.append(getInstanceProperty(REST_partParser, HttpPartParser.class, OpenApiParser.class, resourceResolver, resource, ps))
-				.build()
-				.getParsers()
-				.get(0);
+			partSerializer = getInstanceProperty(REST_partSerializer, HttpPartSerializer.class, OpenApiSerializer.class, resourceResolver, resource, ps);
+			partParser = getInstanceProperty(REST_partParser, HttpPartParser.class, OpenApiParser.class, resourceResolver, resource, ps);
 			jsonSchemaGenerator =
 				JsonSchemaGenerator
 				.create()
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java
index 43ed079..24df668 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestParamDefaults.java
@@ -258,11 +258,16 @@ class RestParamDefaults {
 	static final class HeaderObject extends RestMethodParam {
 		private final HttpPartParser partParser;
 		private final HttpPartSchema schema;
+		private final boolean multi;
 
 		protected HeaderObject(ParamInfo mpi, PropertyStore ps) {
 			super(HEADER, mpi, getName(mpi));
 			this.schema = HttpPartSchema.create(Header.class, mpi);
 			this.partParser = createPartParser(schema.getParser(), ps);
+			this.multi = getMulti(mpi);
+
+			if (multi && ! isCollection(type))
+				throw new InternalServerError("Use of multipart flag on @Header parameter that's not an array or Collection on method ''{0}''", mpi.getMethod());
 		}
 
 		private static String getName(ParamInfo mpi) {
@@ -278,10 +283,18 @@ class RestParamDefaults {
 			return n;
 		}
 
+		private static boolean getMulti(ParamInfo mpi) {
+			for (Header h : mpi.getAnnotations(Header.class))
+				if (h.multi())
+					return true;
+			return false;
+		}
+
 		@Override /* RestMethodParam */
 		public Object resolve(RestRequest req, RestResponse res) throws Exception {
 			HttpPartParserSession ps = partParser == null ? req.getPartParser() : partParser.createPartSession(req.getParserSessionArgs());
-			return req.getHeaders().get(ps, schema, name, type);
+			RequestHeaders rh = req.getHeaders();
+			return multi ? rh.getAll(ps, schema, name, type) : rh.get(ps, schema, name, type);
 		}
 	}
 
@@ -436,7 +449,7 @@ class RestParamDefaults {
 	}
 
 	static final class FormDataObject extends RestMethodParam {
-		private final boolean multiPart;
+		private final boolean multi;
 		private final HttpPartParser partParser;
 		private final HttpPartSchema schema;
 
@@ -444,9 +457,9 @@ class RestParamDefaults {
 			super(FORM_DATA, mpi, getName(mpi));
 			this.schema = HttpPartSchema.create(FormData.class, mpi);
 			this.partParser = createPartParser(schema.getParser(), ps);
-			this.multiPart = schema.getCollectionFormat() == HttpPartSchema.CollectionFormat.MULTI;
+			this.multi = getMulti(mpi) || schema.getCollectionFormat() == HttpPartSchema.CollectionFormat.MULTI;
 
-			if (multiPart && ! isCollection(type))
+			if (multi && ! isCollection(type))
 				throw new InternalServerError("Use of multipart flag on @FormData parameter that's not an array or Collection on method ''{0}''", mpi.getMethod());
 		}
 
@@ -463,17 +476,23 @@ class RestParamDefaults {
 			return n;
 		}
 
+		private static boolean getMulti(ParamInfo mpi) {
+			for (FormData f : mpi.getAnnotations(FormData.class))
+				if (f.multi())
+					return true;
+			return false;
+		}
+
 		@Override /* RestMethodParam */
 		public Object resolve(RestRequest req, RestResponse res) throws Exception {
 			HttpPartParserSession ps = partParser == null ? req.getPartParser() : partParser.createPartSession(req.getParserSessionArgs());
-			if (multiPart)
-				return req.getFormData().getAll(ps, schema, name, type);
-			return req.getFormData().get(ps, schema, name, type);
+			RequestFormData fd = req.getFormData();
+			return multi ? fd.getAll(ps, schema, name, type) : fd.get(ps, schema, name, type);
 		}
 	}
 
 	static final class QueryObject extends RestMethodParam {
-		private final boolean multiPart;
+		private final boolean multi;
 		private final HttpPartParser partParser;
 		private final HttpPartSchema schema;
 
@@ -481,9 +500,9 @@ class RestParamDefaults {
 			super(QUERY, mpi, getName(mpi));
 			this.schema = HttpPartSchema.create(Query.class, mpi);
 			this.partParser = createPartParser(schema.getParser(), ps);
-			this.multiPart = schema.getCollectionFormat() == HttpPartSchema.CollectionFormat.MULTI;
+			this.multi = getMulti(mpi) || schema.getCollectionFormat() == HttpPartSchema.CollectionFormat.MULTI;
 
-			if (multiPart && ! isCollection(type))
+			if (multi && ! isCollection(type))
 				throw new InternalServerError("Use of multipart flag on @Query parameter that's not an array or Collection on method ''{0}''", mpi.getMethod());
 		}
 
@@ -500,12 +519,18 @@ class RestParamDefaults {
 			return n;
 		}
 
+		private static boolean getMulti(ParamInfo mpi) {
+			for (Query q : mpi.getAnnotations(Query.class))
+				if (q.multi())
+					return true;
+			return false;
+		}
+
 		@Override /* RestMethodParam */
 		public Object resolve(RestRequest req, RestResponse res) throws Exception {
 			HttpPartParserSession ps = partParser == null ? req.getPartParser() : partParser.createPartSession(req.getParserSessionArgs());
-			if (multiPart)
-				return req.getQuery().getAll(ps, schema, name, type);
-			return req.getQuery().get(ps, schema, name, type);
+			RequestQuery rq = req.getQuery();
+			return multi ? rq.getAll(ps, schema, name, type) : rq.get(ps, schema, name, type);
 		}
 	}
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
index 18e5574..4efb852 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
@@ -1860,6 +1860,14 @@ public final class RestRequest extends HttpServletRequestWrapper {
 		return inner;
 	}
 
+	<T> ClassMeta<T> getClassMeta(Type type, Type[] args) {
+		return beanSession.getClassMeta(type, args);
+	}
+
+	<T> ClassMeta<T> getClassMeta(Class<T> type) {
+		return beanSession.getClassMeta(type);
+	}
+
 	//-----------------------------------------------------------------------------------------------------------------
 	// Utility methods
 	//-----------------------------------------------------------------------------------------------------------------
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
index f76fc9e..672482a 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.rest;
 
 import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.httppart.HttpPartType.*;
 
 import java.io.*;
 import java.nio.charset.*;
@@ -88,7 +89,7 @@ public final class RestResponse extends HttpServletResponseWrapper {
 			String passThroughHeaders = req.getHeader("x-response-headers");
 			if (passThroughHeaders != null) {
 				HttpPartParser p = context.getPartParser();
-				OMap m = p.createPartSession(req.getParserSessionArgs()).parse(HttpPartType.HEADER, null, passThroughHeaders, context.getClassMeta(OMap.class));
+				OMap m = p.createPartSession(req.getParserSessionArgs()).parse(HEADER, null, passThroughHeaders, context.getClassMeta(OMap.class));
 				for (Map.Entry<String,Object> e : m.entrySet())
 					setHeaderSafe(e.getKey(), e.getValue().toString());
 			}
@@ -587,6 +588,65 @@ public final class RestResponse extends HttpServletResponseWrapper {
 	}
 
 	/**
+	 * Sets a header on the request.
+	 *
+	 * @param name The header name.
+	 * @param value The header value.
+	 * 	<ul>
+	 * 		<li>Can be any POJO.
+	 * 		<li>Converted to a string using the specified part serializer.
+	 * 	</ul>
+	 * @return This object (for method chaining).
+	 * @throws SchemaValidationException Header failed schema validation.
+	 * @throws SerializeException Header could not be serialized.
+	 */
+	public RestResponse header(String name, Object value) throws SchemaValidationException, SerializeException {
+		return header(null, null, name, value);
+	}
+
+	/**
+	 * Sets a header on the request.
+	 *
+	 * @param schema
+	 * 	The schema to use to serialize the header, or <jk>null</jk> to use the default schema.
+	 * @param name The header name.
+	 * @param value The header value.
+	 * 	<ul>
+	 * 		<li>Can be any POJO.
+	 * 		<li>Converted to a string using the specified part serializer.
+	 * 	</ul>
+	 * @return This object (for method chaining).
+	 * @throws SchemaValidationException Header failed schema validation.
+	 * @throws SerializeException Header could not be serialized.
+	 */
+	public RestResponse header(HttpPartSchema schema, String name, Object value) throws SchemaValidationException, SerializeException {
+		return header(null, schema, name, value);
+	}
+
+	/**
+	 * Sets a header on the request.
+	 * @param serializer
+	 * 	The serializer to use to serialize the header, or <jk>null</jk> to use the part serializer on the request.
+	 * @param schema
+	 * 	The schema to use to serialize the header, or <jk>null</jk> to use the default schema.
+	 * @param name The header name.
+	 * @param value The header value.
+	 * 	<ul>
+	 * 		<li>Can be any POJO.
+	 * 		<li>Converted to a string using the specified part serializer.
+	 * 	</ul>
+	 * @return This object (for method chaining).
+	 * @throws SchemaValidationException Header failed schema validation.
+	 * @throws SerializeException Header could not be serialized.
+	 */
+	public RestResponse header(HttpPartSerializerSession serializer, HttpPartSchema schema, String name, Object value) throws SchemaValidationException, SerializeException {
+		if (serializer == null)
+			serializer = request.getPartSerializer();
+		setHeader(name, serializer.serialize(HEADER, schema, value));
+		return this;
+	}
+
+	/**
 	 * Same as {@link #setHeader(String, String)} but header is defined as a response part
 	 *
 	 * @param h Header to set.
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java
index e924609..83b50ed 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java
@@ -69,6 +69,8 @@ public class DefaultHandler implements ResponseHandler {
 			if (isThrowable) {
 				res.setHeaderSafe("Exception-Name", rm.getClassMeta().getName());
 				res.setHeaderSafe("Exception-Message", ((Throwable)o).getMessage());
+				if (req.isDebug())
+					((Throwable)o).printStackTrace();
 			}
 
 			ResponseBeanPropertyMeta stm = rm.getStatusMethod();
diff --git a/pom.xml b/pom.xml
index 04fb109..1409956 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,7 @@
 		<junit.version>4.11</junit.version>
 		<jaxrs.version>1.1.1</jaxrs.version>
 		<servlet.version>3.1.0</servlet.version>
+		<httpcore.version>4.4.13</httpcore.version>
 		<httpclient.version>4.5.6</httpclient.version>
 		<jetty.version>9.4.13.v20181111</jetty.version>
 		<juneau.compare.version>8.0.0</juneau.compare.version>
@@ -90,6 +91,11 @@
 			</dependency>
 			<dependency>
 				<groupId>org.apache.httpcomponents</groupId>
+				<artifactId>httpcore</artifactId>
+				<version>${httpcore.version}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.apache.httpcomponents</groupId>
 				<artifactId>httpmime</artifactId>
 				<version>${httpclient.version}</version>
 			</dependency>