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 2017/03/27 01:19:24 UTC

incubator-juneau git commit: Add support for exception passing in proxy interfaces.

Repository: incubator-juneau
Updated Branches:
  refs/heads/master ca31b2388 -> b1a30b033


Add support for exception passing in proxy interfaces.

Project: http://git-wip-us.apache.org/repos/asf/incubator-juneau/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-juneau/commit/b1a30b03
Tree: http://git-wip-us.apache.org/repos/asf/incubator-juneau/tree/b1a30b03
Diff: http://git-wip-us.apache.org/repos/asf/incubator-juneau/diff/b1a30b03

Branch: refs/heads/master
Commit: b1a30b03340213cbd50a58a7eb06a0d5a3be8663
Parents: ca31b23
Author: JamesBognar <ja...@apache.org>
Authored: Sun Mar 26 18:19:20 2017 -0700
Committer: JamesBognar <ja...@apache.org>
Committed: Sun Mar 26 18:19:20 2017 -0700

----------------------------------------------------------------------
 juneau-core/src/main/javadoc/overview.html      | 108 +++++++++++++------
 .../org/apache/juneau/rest/client/RestCall.java |   4 +-
 .../juneau/rest/client/RestCallException.java   |  59 ++++++++++
 .../apache/juneau/rest/client/RestClient.java   |   6 +-
 .../apache/juneau/rest/test/InterfaceProxy.java |  14 +++
 .../rest/test/InterfaceProxyResource.java       |  80 ++++++++++----
 .../juneau/rest/test/InterfaceProxyTest.java    |  27 ++++-
 .../org/apache/juneau/rest/RestContext.java     |  14 ++-
 8 files changed, 252 insertions(+), 60 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/b1a30b03/juneau-core/src/main/javadoc/overview.html
----------------------------------------------------------------------
diff --git a/juneau-core/src/main/javadoc/overview.html b/juneau-core/src/main/javadoc/overview.html
index b9af064..5c80d9a 100644
--- a/juneau-core/src/main/javadoc/overview.html
+++ b/juneau-core/src/main/javadoc/overview.html
@@ -2250,56 +2250,95 @@
 			use and allowing much more flexibility.
 	</p>
 	<p>
-		Remoteable Services are implemented through a combination of the Server and Client libraries.
+		The remote proxy interface API allows you to invoke server-side POJO methods on the client side using REST as the communications protocol:
 	</p>
-	<ul class='spaced-list'>
-		<li>Proxy interfaces are retrieved using the {@link org.apache.juneau.rest.client.RestClient#getRemoteableProxy(Class)} method.
-		<li>The {@link org.apache.juneau.rest.client.RestClientBuilder#remoteableServletUri(String)} method is used to specify the location
-			of the remoteable services servlet running on the server.
-		<li>The {@link org.apache.juneau.rest.remoteable.RemoteableServlet} class is a specialized subclass of {@link org.apache.juneau.rest.RestServlet} that provides a full-blown
-			REST interface for calling interfaces remotely. 
-	</ul>
-	<p>
-		In this example, you have the following interface defined that you want to call from the client side against
-			a POJO on the server side (i.e. a Remoteable Service):
 	<p class='bcode'>
-	<jk>public interface</jk> IAddressBook {
-		Person createPerson(CreatePerson cp) <jk>throws</jk> Exception;
-	}
-	</p>			
+ 	<jc>// Get an interface proxy.</jc>
+ 	IAddressBook ab = restClient.getRemoteableProxy(IAddressBook.<jk>class</jk>);
+	
+	<jc>// Invoke a method on the server side and get the returned result.</jc>
+	Person p = ab.createPerson(
+		<jk>new</jk> Person(
+			<js>"John Smith"</js>, 
+			<js>"Aug 1, 1999"</js>,
+			<jk>new</jk> Address(<js>"My street"</js>, <js>"My city"</js>, <js>"My state"</js>, 12345, <jk>true</jk>)
+		)
+	);
+	</p>
 	<p>
-		The client side code for invoking this method is shown below:
+		There are two ways to expose remoteable proxies on the server side:
+	</p>
+	<ol>
+		<li>Extending from <code>RemoteableServlet</code>.
+		<li>Using a <code><ja>@RestMethod</ja>(name=<js>"PROXY"</js>)</code> annotation on a Java method.
+	</ol>
+	<p>
+		The <code>RemoteableServlet</code> class is a simple specialized servlet with an abstract <code>getServiceMap()</code>
+		method to define the server-side POJOs:
 	</p>
 	<p class='bcode'>
-	<jc>// Create a RestClient using JSON for serialization, and point to the server-side remoteable servlet.</jc>
-	RestClient client = <jk>new</jk> RestClientBuilder()
-		.remoteableServletUri(<js>"https://localhost:9080/juneau/sample/remoteable"</js>)
-		.build();
+	<ja>@RestResource</ja>(
+		path=<js>"/remote"</js>
+	)
+	<jk>public class</jk> SampleRemoteableServlet <jk>extends</jk> RemoteableServlet {
 	
-	<jc>// Create a proxy interface.</jc>
-	IAddressBook ab = client.getRemoteableProxy(IAddressBook.<jk>class</jk>);
+		<jc>// Our server-side POJO.</jc>
+		AddressBook <jf>addressBook</jf> = <jk>new</jk> AddressBook();
 	
-	<jc>// Invoke a method on the server side and get the returned result.</jc>
-	Person p = ab.createPerson(
-		<jk>new</jk> CreatePerson(<js>"Test Person"</js>,
-			AddressBook.<jsm>toCalendar</jsm>(<js>"Aug 1, 1999"</js>),
-			<jk>new</jk> CreateAddress(<js>"Test street"</js>, <js>"Test city"</js>, <js>"Test state"</js>, 12345, <jk>true</jk>))
-	);
+		<ja>@Override</ja> <jc>/* RemoteableServlet */</jc>
+		<jk>protected</jk> Map&lt;Class&lt;?&gt;,Object&gt; getServiceMap() <jk>throws</jk> Exception {
+			Map&lt;Class&lt;?&gt;,Object&gt; m = <jk>new</jk> LinkedHashMap&lt;Class&lt;?&gt;,Object&gt;();
+	
+			<jc>// In this simplified example, we expose the same POJO service under two different interfaces.
+			// One is IAddressBook which only exposes methods defined on that interface, and
+			// the other is AddressBook itself which exposes all methods defined on the class itself (dangerous!).</jc>
+			m.put(IAddressBook.<jk>class</jk>, <jf>addressBook</jf>);
+			m.put(AddressBook.<jk>class</jk>, <jf>addressBook</jf>);
+			<jk>return</jk> m;
+		}
+	}
 	</p>
 	<p>
-		The requirements for a method to be callable through a remoteable service are:
+		The <code><ja>@RestMethod</ja>(name=<js>"PROXY"</js>)</code> approach is easier if you only have a single interface you want to expose.  
+		You simply define a Java method whose return type is an interface, and return the implementation of that interface:
+	</p>
+	<p class='bcode'>
+	<jc>// Our exposed proxy object.</jc>
+	<ja>@RestMethod</ja>(name=<js>"PROXY"</js>, path=<js>"/addressbookproxy/*"</js>)
+	<jk>public</jk> IAddressBook getProxy() {
+		<jk>return</jk> addressBook;
+	}
 	</p>
-	<ul class='spaced-list'>
-		<li>The method must be public.
-		<li>The parameter and return types must be <a href='#Core.PojoCategories'>serializable and parsable</a>.
-	</ul>
 	<p>
-		One significant feature is that the remoteable services servlet is a full-blown REST interface.  
+		In either case, the proxy communications layer is pure REST.   
+		Parameters passed in on the client side are serialized as an HTTP POST, parsed on the
+		server side, and then passed to the invocation method.  The returned POJO is then marshalled back as an HTTP response.
 		Therefore, in cases where the interface classes are not available on the client side,
 			the same method calls can be made through pure REST calls.  
 		This can also aid significantly in debugging, since calls to the remoteable service
 			can be made directly from a browser with no coding involved.
 	</p>
+	<p>
+		The parameters and return types of the Java methods can be any of the supported serializable and parsable types in <a class='doclink' href='#Core.PojoCategories'>POJO Categories</a>.
+		This ends up being WAY more flexible than other proxy interfaces since Juneau can handle so may POJO types out-of-the-box.
+		Most of the time you don't even need to modify your existing Java implementation code.
+	</p>
+	<p>
+		The RemoteableServlet class itself shows how sophisticated REST interfaces can be built on the Juneau RestServlet
+		API using very little code.  The RemoteableServlet class itself consists of only 53 lines of code, yet is
+		a sophisticated discoverable and self-documenting REST interface.  And since the remote proxy API is built on top 
+		of REST, it can be debugged using just a browser.
+	</p>
+	<p>
+		The requirements for a method to be callable through a remoteable service are:
+	</p>
+	<ul class='spaced-list'>
+		<li>The method must be public.
+		<li>The parameter and return types must be <a href='#Core.PojoCategories'>serializable and parsable</a>.
+			Parameterized types are supported.
+		<li>Methods can throw <code>Throwables</code> with public no-arg or single-arg-string constructors which will be automatically 
+			recreated on the client side.
+	</ul>
 	<h6 class='topic'>Additional Information</h6>
 	<ul class='javahierarchy'>
 		<li class='p'><a class='doclink' href='org/apache/juneau/server/remoteable/package-summary.html#TOC'>org.apache.juneau.rest.remoteable</a> - Juneau Remoteable API Javadocs.
@@ -5788,6 +5827,7 @@
 	IAddressBook ab = client.getRemoteableProxy(IAddressBook.<jk>class</jk>, <js>"/addressBook/myproxy"</js>);
 			</p>
 				See {@link org.apache.juneau.rest.annotation.RestMethod#name()} for more information.
+			<li>Updated doc: <a class='doclink' href='#Remoteable'>6 - Remoteable Services</a>
 			<li>{@link org.apache.juneau.rest.RestRequest#toString()} can be called at any time to view the headers and content of the request
 				without affecting functionality.  Very useful for debugging.
 		</ul>

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/b1a30b03/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
----------------------------------------------------------------------
diff --git a/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java b/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
index 88e892d..ab9e010 100644
--- a/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
+++ b/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCall.java
@@ -953,7 +953,9 @@ public final class RestCall {
 			String method = request.getMethod();
 			sc = sl.getStatusCode(); // Read it again in case it was changed by one of the interceptors.
 			if (sc >= 400 && ! ignoreErrors)
-				throw new RestCallException(sc, sl.getReasonPhrase(), method, request.getURI(), getResponseAsString()).setHttpResponse(response);
+				throw new RestCallException(sc, sl.getReasonPhrase(), method, request.getURI(), getResponseAsString())
+					.setServerException(response.getFirstHeader("Exception-Name"), response.getFirstHeader("Exception-Message"), response.getFirstHeader("Exception-Trace"))
+					.setHttpResponse(response);
 			if ((sc == 307 || sc == 302) && allowRedirectsOnPosts && method.equalsIgnoreCase("POST")) {
 				if (redirectOnPostsTries-- < 1)
 					throw new RestCallException(sc, "Maximum number of redirects occurred.  Location header: " + response.getFirstHeader("Location"), method, request.getURI(), getResponseAsString());

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/b1a30b03/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCallException.java
----------------------------------------------------------------------
diff --git a/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCallException.java b/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCallException.java
index d986b46..d4579eb 100644
--- a/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCallException.java
+++ b/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestCallException.java
@@ -15,6 +15,7 @@ package org.apache.juneau.rest.client;
 import static java.lang.String.*;
 
 import java.io.*;
+import java.lang.reflect.*;
 import java.net.*;
 import java.util.regex.*;
 
@@ -35,6 +36,9 @@ public final class RestCallException extends IOException {
 	HttpResponseException e;
 	private HttpResponse httpResponse;
 
+	@SuppressWarnings("unused")
+	private String serverExceptionName, serverExceptionMessage, serverExceptionTrace;
+
 
 	/**
 	 * Constructor.
@@ -92,6 +96,61 @@ public final class RestCallException extends IOException {
 	}
 
 	/**
+	 * Sets the server-side exception details.
+	 *
+	 * @param exceptionName The <code>Exception-Name:</code> header specifying the full name of the exception.
+	 * @param exceptionMessage The <code>Exception-Message:</code> header specifying the message returned by {@link Throwable#getMessage()}.
+	 * @param exceptionTrace The stack trace of the exception returned by {@link Throwable#printStackTrace()}.
+	 * @return This object (for method chaining).
+	 */
+	protected RestCallException setServerException(Header exceptionName, Header exceptionMessage, Header exceptionTrace) {
+		if (exceptionName != null)
+			serverExceptionName = exceptionName.getValue();
+		if (exceptionMessage != null)
+			serverExceptionMessage = exceptionMessage.getValue();
+		if (exceptionTrace != null)
+			serverExceptionTrace = exceptionTrace.getValue();
+		return this;
+	}
+
+	/**
+	 * Tries to reconstruct and re-throw the server-side exception.
+	 * <p>
+	 * The exception is based on the following HTTP response headers:
+	 * <ul>
+	 * 	<li><code>Exception-Name:</code> - The full class name of the exception.
+	 * 	<li><code>Exception-Message:</code> - The message returned by {@link Throwable#getMessage()}.
+	 * 	<li><code>Exception-Trace:</code> - The stack trace of the exception returned by {@link Throwable#printStackTrace()}.
+	 * </ul>
+	 * <p>
+	 * Does nothing if the server-side exception could not be reconstructed.
+	 * <p>
+	 * Currently only supports <code>Throwables</code> with either a public no-arg constructor
+	 * or a public constructor that takes in a simple string message.
+	 *
+	 * @param cl The classloader to use to resolve the throwable class name.
+	 * @throws Throwable If the throwable could be reconstructed.
+	 */
+	protected void throwServerException(ClassLoader cl) throws Throwable {
+		if (serverExceptionName != null) {
+			Throwable t = null;
+			try {
+				Class<?> exceptionClass = cl.loadClass(serverExceptionName);
+				Constructor<?> c = ClassUtils.findPublicConstructor(exceptionClass, String.class);
+				if (c != null)
+					t = (Throwable)c.newInstance(serverExceptionMessage);
+				if (t == null) {
+					c = ClassUtils.findPublicConstructor(exceptionClass);
+					if (c != null)
+						t = (Throwable)c.newInstance();
+				}
+			} catch (Exception e2) { /* Ignore */ }
+			if (t != null)
+				throw t;
+		}
+	}
+
+	/**
 	 * Sets the HTTP response object that caused this exception.
 	 *
 	 * @param httpResponse The HTTP respose object.

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/b1a30b03/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
----------------------------------------------------------------------
diff --git a/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java b/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
index 37e8f33..d926c8e 100644
--- a/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
+++ b/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClient.java
@@ -455,7 +455,7 @@ public class RestClient extends CoreObject {
 					final String uri = toURI(proxyUrl).toString();
 
 					@Override /* InvocationHandler */
-					public Object invoke(Object proxy, Method method, Object[] args) {
+					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 
 						// Constructing this string each time can be time consuming, so cache it.
 						String u = uriCache.get(method);
@@ -468,6 +468,10 @@ public class RestClient extends CoreObject {
 
 						try {
 							return doPost(u, args).serializer(serializer).parser(parser).getResponse(method.getGenericReturnType());
+						} catch (RestCallException e) {
+							// Try to throw original exception if possible.
+							e.throwServerException(interfaceClass.getClassLoader());
+							throw new RuntimeException(e);
 						} catch (Exception e) {
 							throw new RuntimeException(e);
 						}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/b1a30b03/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxy.java
----------------------------------------------------------------------
diff --git a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxy.java b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxy.java
index 762886e..67e2f5f 100644
--- a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxy.java
+++ b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxy.java
@@ -38,6 +38,9 @@ public interface InterfaceProxy {
 	Map<String,List<Bean>> returnBeanListMap();
 	Map<Integer,List<Bean>> returnBeanListMapIntegerKeys();
 
+	void throwException1() throws InterfaceProxyException1;
+	void throwException2() throws InterfaceProxyException2;
+
 	void setNothing();
 	void setInt(int x);
 	void setInteger(Integer x);
@@ -67,4 +70,15 @@ public interface InterfaceProxy {
 			return this;
 		}
 	}
+	
+	@SuppressWarnings("serial")
+	public static class InterfaceProxyException1 extends Throwable {
+		public InterfaceProxyException1(String msg) {
+			super(msg);
+		}
+	}
+
+	@SuppressWarnings("serial")
+	public static class InterfaceProxyException2 extends Throwable {		
+	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/b1a30b03/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxyResource.java
----------------------------------------------------------------------
diff --git a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxyResource.java b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxyResource.java
index 57fa638..8e226a0 100644
--- a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxyResource.java
+++ b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/InterfaceProxyResource.java
@@ -37,42 +37,84 @@ public class InterfaceProxyResource extends RestServletJenaDefault {
 	public InterfaceProxy getProxy() {
 		return new InterfaceProxy() {
 			@Override
-			public void returnVoid() {}
+			public void returnVoid() {
+			}
 			@Override
-			public Integer returnInteger() { return 1;}
+			public Integer returnInteger() {
+				return 1;
+			}
 			@Override
-			public int returnInt() { return 1; }
+			public int returnInt() {
+				return 1;
+			}
 			@Override
-			public boolean returnBoolean() { return true; }
+			public boolean returnBoolean() {
+				return true;
+			}
 			@Override
-			public float returnFloat() { return 1f; }
+			public float returnFloat() {
+				return 1f;
+			}
 			@Override
-			public Float returnFloatObject() { return 1f; }
+			public Float returnFloatObject() {
+				return 1f;
+			}
 			@Override
-			public String returnString() { return "foobar"; }
+			public String returnString() {
+				return "foobar";
+			}
 			@Override
-			public String returnNullString() { return null; }
+			public String returnNullString() {
+				return null;
+			}
 			@Override
-			public int[] returnIntArray() { return new int[]{1,2}; }
+			public int[] returnIntArray() {
+				return new int[]{1,2};
+			}
 			@Override
-			public String[] returnStringArray() { return new String[]{"foo","bar",null};}
+			public String[] returnStringArray() {
+				return new String[]{"foo","bar",null};
+			}
 			@Override
-			public List<Integer> returnIntegerList() { return Arrays.asList(new Integer[]{1,2}); }
+			public List<Integer> returnIntegerList() {
+				return Arrays.asList(new Integer[]{1,2});
+			}
 			@Override
-			public List<String> returnStringList() { return Arrays.asList(new String[]{"foo","bar",null}); }
+			public List<String> returnStringList() {
+				return Arrays.asList(new String[]{"foo","bar",null});
+			}
 			@Override
-			public Bean returnBean() { return new Bean().init(); }
+			public Bean returnBean() {
+				return new Bean().init();
+			}
 			@Override
-			public Bean[] returnBeanArray() { return new Bean[]{new Bean().init()}; }
+			public Bean[] returnBeanArray() {
+				return new Bean[]{new Bean().init()};
+			}
 			@Override
-			public List<Bean> returnBeanList() { return Arrays.asList(new Bean().init()); }
+			public List<Bean> returnBeanList() {
+				return Arrays.asList(new Bean().init());
+			}
 			@Override
-			public Map<String,Bean> returnBeanMap() { return new HashMap<String,Bean>(){{put("foo",new Bean().init());}}; }
+			public Map<String,Bean> returnBeanMap() {
+				return new HashMap<String,Bean>(){{put("foo",new Bean().init());}};
+			}
 			@Override
-			public Map<String,List<Bean>> returnBeanListMap() { return new HashMap<String,List<Bean>>(){{put("foo",Arrays.asList(new Bean().init()));}}; }
+			public Map<String,List<Bean>> returnBeanListMap() {
+				return new HashMap<String,List<Bean>>(){{put("foo",Arrays.asList(new Bean().init()));}};
+			}
 			@Override
-			public Map<Integer,List<Bean>> returnBeanListMapIntegerKeys() { return new HashMap<Integer,List<Bean>>(){{put(1,Arrays.asList(new Bean().init()));}}; }
-
+			public Map<Integer,List<Bean>> returnBeanListMapIntegerKeys() {
+				return new HashMap<Integer,List<Bean>>(){{put(1,Arrays.asList(new Bean().init()));}};
+			}
+			@Override
+			public void throwException1() throws InterfaceProxy.InterfaceProxyException1 {
+				throw new InterfaceProxy.InterfaceProxyException1("foo");
+			}
+			@Override
+			public void throwException2() throws InterfaceProxy.InterfaceProxyException2 {
+				throw new InterfaceProxy.InterfaceProxyException2();
+			}
 			@Override
 			public void setNothing() {
 			}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/b1a30b03/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/InterfaceProxyTest.java
----------------------------------------------------------------------
diff --git a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/InterfaceProxyTest.java b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/InterfaceProxyTest.java
index 2a7fdfd..74b59c6 100644
--- a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/InterfaceProxyTest.java
+++ b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/InterfaceProxyTest.java
@@ -160,6 +160,25 @@ public class InterfaceProxyTest extends RestTestcase {
 	}
 
 	@Test
+	public void throwException1() {
+		try {
+			getProxy().throwException1();
+			fail("Exception expected");
+		} catch (InterfaceProxy.InterfaceProxyException1 e) {
+			assertEquals("foo", e.getMessage());
+		}
+	}
+
+	@Test
+	public void throwException2() {
+		try {
+			getProxy().throwException2();
+			fail("Exception expected");
+		} catch (InterfaceProxy.InterfaceProxyException2 e) {
+		}
+	}
+
+	@Test
 	public void setNothing() {
 		getProxy().setNothing();
 	}
@@ -174,8 +193,8 @@ public class InterfaceProxyTest extends RestTestcase {
 		try {
 			getProxy().setInt(2);
 			fail("Exception expected");
-		} catch (Exception e) {
-			// Good.
+		} catch (AssertionError e) { // AssertionError thrown on server side.
+			assertEquals("expected:<1> but was:<2>", e.getMessage());
 		}
 	}
 
@@ -214,8 +233,8 @@ public class InterfaceProxyTest extends RestTestcase {
 		try {
 			getProxy().setNullString("foo");
 			fail("Exception expected");
-		} catch (Exception e) {
-			// Good.
+		} catch (AssertionError e) { // AssertionError thrown on server side.
+			assertEquals("expected null, but was:<foo>", e.getLocalizedMessage());
 		}
 	}
 

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/b1a30b03/juneau-rest/src/main/java/org/apache/juneau/rest/RestContext.java
----------------------------------------------------------------------
diff --git a/juneau-rest/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/src/main/java/org/apache/juneau/rest/RestContext.java
index 89abdc4..f1e0228 100644
--- a/juneau-rest/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -432,7 +432,19 @@ public final class RestContext extends Context {
 												// Parse the args and invoke the method.
 												Parser p = req.getParser();
 												Object input = p.isReaderParser() ? req.getReader() : req.getInputStream();
-												res.setOutput(m.invoke(o, p.parseArgs(input, m.getGenericParameterTypes())));
+												Object output = null;
+												try {
+													output = m.invoke(o, p.parseArgs(input, m.getGenericParameterTypes()));
+												} catch (InvocationTargetException e) {
+													res.setHeader("Exception-Name", e.getCause().getClass().getName());
+													res.setHeader("Exception-Message", e.getCause().getMessage());
+													throw e;
+												} catch (Exception e) {
+													res.setHeader("Exception-Name", e.getClass().getName());
+													res.setHeader("Exception-Message", e.getCause().getMessage());
+													throw e;
+												}
+												res.setOutput(output);
 												return SC_OK;
 											} catch (Exception e) {
 												throw new RestException(SC_INTERNAL_SERVER_ERROR, e);