Skip to content

Commit

Permalink
[RESTEASY-2728] Clients running in a resource method throw safer WebA…
Browse files Browse the repository at this point in the history
…pplicationExceptions (#2623)

[RESTEASY-2728] Added arquillian tests.

[RESTEASY-2728] Correct serialVersionUID in some of the new ClientWebApplicationExceptions.

[RESTEASY-2728] Adding documentation to User Guide.

[RESTEASY-2728] Make the custom WebApplicationException's wrap the exception they're replacing. Also create a sanitized response for the exceptions. Add an API used to wrap/unwrap the exceptions.

https://issues.redhat.com/browse/RESTEASY-2728

[RESTEASY-2728] 1) MP REST Client DefaultResponseExceptionMapper can handle RedirectException;
2) Fix integration tests.

[RESTEASY-2728] Modified docbook.
  • Loading branch information
ronsigal committed Dec 6, 2020
1 parent 3b2dc7e commit 771f2c6
Show file tree
Hide file tree
Showing 34 changed files with 3,183 additions and 17 deletions.
100 changes: 100 additions & 0 deletions docbook/reference/en/en-US/modules/ExceptionMappers.xml
Expand Up @@ -131,6 +131,106 @@ If there is an ExceptionMapper for wrapped exception, then that is used to handl
</tbody>
</tgroup>
</table>
</sect1>
<sect1 id="ResteasyWebApplicationException">
<title>Resteasy WebApplicationExceptions</title>
<para>Suppose a client at local.com calls the following resource method:</para>
<programlisting>
@GET
@Path("remote")
public String remote() throws Exception {
Client client = ClientBuilder.newClient();
return client.target("http://third.party.com/exception").request().get(String.class);
}
</programlisting>
<para>If the call to http://third.party.com returns a status code 3xx, 4xx, or 5xx, then the
<classname>Client</classname> is obliged by the JAX-RS
specification to throw a <classname>WebApplicationException</classname>. Moreover, if the
<classname>WebApplicationException</classname> contains a <classname>Response</classname>, which
it normally would in RESTEasy, the server runtime is obliged by the JAX-RS specification to return that
<classname>Response</classname>.
As a result, information from the server at third.party.com, e.g., headers and body, will get sent back to
local.com. The problem is that that information could be, at best, meaningless to the client
and, at worst, a security breach.</para>

<para>RESTEasy has a solution that works around the problem and still conforms to the JAX-RS specification.
In particular, for each <classname>WebApplicationException</classname> it defines a new subclass:</para>

<programlisting>
WebApplicationException
+-ResteasyWebApplicationException
+-ClientErrorException
| +-ResteasyClientErrorException
| +-BadRequestException
| | +-ResteasyBadRequestException
| +-ForbiddenException
| | +-ResteasyForbiddenException
| +-NotAcceptableException
| | +-ResteasyNotAcceptableException
| +-NotAllowedException
| | +-ResteasyNotAllowedException
| +-NotAuthorizedException
| | +-ResteasyNotAuthorizedException
| +-NotFoundException
| | +-ResteasyNotFoundException
| +-NotSupportedException
| | +-ResteasyNotSupportedException
+-RedirectionException
| +-ResteasyRedirectionException
+-ServerErrorException
| +-ResteasyServerErrorException
| +-InternalServerErrorException
| | +-ResteasyInternalServerErrorException
| +-ServiceUnavailableException
| | +-ResteasyServiceUnavailableException
</programlisting>

<para>The new <classname>Exception</classname>s play the same role as the original ones,
but RESTEasy treats them slightly differently. When a <classname>Client</classname> detects
that it is running in the context of a resource method, it will throw one of the new
<classname>Exception</classname>s. However, instead of storing the original <classname>Response</classname>,
it stores a "sanitized" version of the <classname>Response</classname>, in which only the status and
the Allow and Content-Type headers are preserved. The original <classname>WebApplicationException</classname>,
and therefore the original <classname>Response</classname>, can be accessed in one of two ways:</para>

<programlisting>
// Create a NotAcceptableException.
NotAcceptableException nae = new NotAcceptableException(Response.status(406).entity("ooops").build());

// Wrap the NotAcceptableException in a ResteasyNotAcceptableException.
ResteasyNotAcceptableException rnae = (ResteasyNotAcceptableException) WebApplicationExceptionWrapper.wrap(nae);

// Extract the original NotAcceptableException using instance method.
NotAcceptableException nae2 = rnae.unwrap();
Assert.assertEquals(nae, nae2);

// Extract the original NotAcceptableException using class method.
NotAcceptableException nae3 = (NotAcceptableException) WebApplicationExceptionWrapper.unwrap(nae); // second way
Assert.assertEquals(nae, nae3);
</programlisting>

<para>Note that this change is intended to introduce a safe default behavior in the case that
the <classname>Exception</classname> generated by the remote call is allowed to make its way up
to the server runtime. It is considered a good practice, though, to catch the
<classname>Exception</classname> and treat it in some appropriate manner:</para>

<programlisting>
@GET
@Path("remote/{i}")
public String remote(@PathParam("i") String i) throws Exception {
Client client = ClientBuilder.newClient();
try {
return client.target("http://remote.com/exception/" + i).request().get(String.class);
} catch (WebApplicationException wae) {
...
}
}
</programlisting>

<para><emphasis role="bold">Note.</emphasis> While RESTEasy will default to the new, safer behavior, the original behavior can
be restored by setting the configuration parameter "resteasy.original.webapplicationexception.behavior"
to "true".</para>

</sect1>
<sect1 id="overring_resteasy_exceptions">
<title>Overriding RESTEasy Builtin Exceptions</title>
Expand Down
15 changes: 15 additions & 0 deletions docbook/reference/en/en-US/modules/Installation_Configuration.xml
Expand Up @@ -1062,6 +1062,21 @@ String s = config.getOptionalValue("prop_name", String.class).orElse("d'oh");
will not occur.
</entry>
</row>
<row>
<entry>
resteasy.original.webapplicationexception.behavior
</entry>
<entry>
false
</entry>
<entry>
When set to "true", this parameter will restore the original behavior in which
a Client running in a resource method will throw a JAX-RS WebApplicationException
instead of a ResteasyWebApplicationException. See section
<link linkend='ResteasyWebApplicationException'>ResteasyWebApplicationException</link>
for more information.
</entry>
</row>
</tbody>
</tgroup>
</table>
Expand Down
Expand Up @@ -412,6 +412,13 @@ of a registered <classname>ResponseExceptionMapper</classname>, a default <class
will map any response with status >= 400 to a <classname>WebApplicationException</classname>.
</para>

<para><emphasis role="bold">Note.</emphasis> Related to the change described in section
<link linkend="ResteasyWebApplicationException">"Resteasy WebApplicationExceptions"</link>,
RESTEasy's default <classname>ResponseExceptionMapper</classname> will map a response with status >= 300.
The original behavior can be restored by setting the configuration parameter "resteasy.original.webapplicationexception.behavior"
to "true".
</para>

</sect1>

<sect1>
Expand Down
@@ -1,6 +1,12 @@
package org.jboss.resteasy.microprofile.client;

import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
import org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.microprofile.config.ResteasyConfig;
import org.jboss.resteasy.microprofile.config.ResteasyConfigFactory;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.spi.ResteasyProviderFactory;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
Expand All @@ -13,12 +19,16 @@ public Throwable toThrowable(Response response) {
try {
response.bufferEntity();
} catch (Exception ignored) {}
return new WebApplicationException("Unknown error, status code " + response.getStatus(), response);
return WebApplicationExceptionWrapper.wrap(new WebApplicationException("Unknown error, status code " + response.getStatus(), response));
}

@Override
public boolean handles(int status, MultivaluedMap headers) {
return status >= 400;
final ResteasyConfig config = ResteasyConfigFactory.getConfig();
final boolean originalBehavior = Boolean.parseBoolean(config.getValue(ResteasyContextParameters.RESTEASY_ORIGINAL_WEBAPPLICATIONEXCEPTION_BEHAVIOR,
ResteasyConfig.SOURCE.SERVLET_CONTEXT, "false"));
final boolean serverSide = ResteasyProviderFactory.searchContextData(Dispatcher.class) != null;
return status >= (originalBehavior || !serverSide ? 400 : 300);
}

@Override
Expand Down
Expand Up @@ -44,6 +44,7 @@
import javax.ws.rs.ext.Providers;
import javax.ws.rs.ext.WriterInterceptor;

import org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper;
import org.jboss.resteasy.client.jaxrs.AsyncClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.ClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
Expand Down Expand Up @@ -109,6 +110,7 @@ public ClientInvocation(final ResteasyClient client, final URI uri, final Client
* @param <T> type
* @return extracted result of type T
*/
@SuppressWarnings("unchecked")
public static <T> T extractResult(GenericType<T> responseType, Response response, Annotation[] annotations)
{
int status = response.getStatus();
Expand Down Expand Up @@ -191,7 +193,9 @@ public static <T> T extractResult(GenericType<T> responseType, Response response
}
}
if (status >= 300 && status < 400)
throw new RedirectionException(response);
{
throw WebApplicationExceptionWrapper.wrap(new RedirectionException(response));
}

return handleErrorStatus(response);
}
Expand All @@ -217,33 +221,33 @@ public static <T> T handleErrorStatus(Response response)
switch (status)
{
case 400 :
throw new BadRequestException(response);
throw WebApplicationExceptionWrapper.wrap(new BadRequestException(response));
case 401 :
throw new NotAuthorizedException(response);
throw WebApplicationExceptionWrapper.wrap(new NotAuthorizedException(response));
case 403 :
throw new ForbiddenException(response);
throw WebApplicationExceptionWrapper.wrap(new ForbiddenException(response));
case 404 :
throw new NotFoundException(response);
throw WebApplicationExceptionWrapper.wrap(new NotFoundException(response));
case 405 :
throw new NotAllowedException(response);
throw WebApplicationExceptionWrapper.wrap(new NotAllowedException(response));
case 406 :
throw new NotAcceptableException(response);
throw WebApplicationExceptionWrapper.wrap(new NotAcceptableException(response));
case 415 :
throw new NotSupportedException(response);
throw WebApplicationExceptionWrapper.wrap(new NotSupportedException(response));
case 500 :
throw new InternalServerErrorException(response);
throw WebApplicationExceptionWrapper.wrap(new InternalServerErrorException(response));
case 503 :
throw new ServiceUnavailableException(response);
throw WebApplicationExceptionWrapper.wrap(new ServiceUnavailableException(response));
default :
break;
}

if (status >= 400 && status < 500)
throw new ClientErrorException(response);
throw WebApplicationExceptionWrapper.wrap(new ClientErrorException(response));
if (status >= 500)
throw new ServerErrorException(response);
throw WebApplicationExceptionWrapper.wrap(new ServerErrorException(response));

throw new WebApplicationException(response);
throw WebApplicationExceptionWrapper.wrap(new WebApplicationException(response));
}

public ClientConfiguration getClientConfiguration()
Expand Down
@@ -0,0 +1,25 @@
package org.jboss.resteasy.client.exception;

import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;

import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Response;

/**
* Wraps a {@link BadRequestException} with a {@linkplain #sanitize(Response) sanitized} response.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class ResteasyBadRequestException extends BadRequestException implements WebApplicationExceptionWrapper<BadRequestException> {
private final BadRequestException wrapped;

ResteasyBadRequestException(final BadRequestException wrapped) {
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
this.wrapped = wrapped;
}

@Override
public BadRequestException unwrap() {
return wrapped;
}
}
@@ -0,0 +1,27 @@
package org.jboss.resteasy.client.exception;


import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;

import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;

/**
* Wraps a {@link ClientErrorException} with a {@linkplain #sanitize(Response) sanitized} response.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class ResteasyClientErrorException extends ClientErrorException implements WebApplicationExceptionWrapper<ClientErrorException> {

private final ClientErrorException wrapped;

ResteasyClientErrorException(final ClientErrorException wrapped) {
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
this.wrapped = wrapped;
}

@Override
public ClientErrorException unwrap() {
return wrapped;
}
}
@@ -0,0 +1,25 @@
package org.jboss.resteasy.client.exception;

import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;

import javax.ws.rs.ForbiddenException;
import javax.ws.rs.core.Response;

/**
* Wraps a {@link ForbiddenException} with a {@linkplain #sanitize(Response) sanitized} response.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class ResteasyForbiddenException extends ForbiddenException implements WebApplicationExceptionWrapper<ForbiddenException> {
private final ForbiddenException wrapped;

ResteasyForbiddenException(final ForbiddenException wrapped) {
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
this.wrapped = wrapped;
}

@Override
public ForbiddenException unwrap() {
return wrapped;
}
}
@@ -0,0 +1,25 @@
package org.jboss.resteasy.client.exception;

import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;

import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.core.Response;

/**
* Wraps a {@link InternalServerErrorException} with a {@linkplain #sanitize(Response) sanitized} response.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class ResteasyInternalServerErrorException extends InternalServerErrorException implements WebApplicationExceptionWrapper<InternalServerErrorException> {
private final InternalServerErrorException wrapped;

ResteasyInternalServerErrorException(final InternalServerErrorException wrapped) {
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
this.wrapped = wrapped;
}

@Override
public InternalServerErrorException unwrap() {
return wrapped;
}
}
@@ -0,0 +1,25 @@
package org.jboss.resteasy.client.exception;

import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;

import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.core.Response;

/**
* Wraps a {@link NotAcceptableException} with a {@linkplain #sanitize(Response) sanitized} response.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class ResteasyNotAcceptableException extends NotAcceptableException implements WebApplicationExceptionWrapper<NotAcceptableException> {
private final NotAcceptableException wrapped;

ResteasyNotAcceptableException(final NotAcceptableException wrapped) {
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
this.wrapped = wrapped;
}

@Override
public NotAcceptableException unwrap() {
return wrapped;
}
}

0 comments on commit 771f2c6

Please sign in to comment.