8000 [RESTEASY-2728] Clients running in a resource method throw safer WebA… · resteasy/resteasy@771f2c6 · GitHub 8000
[go: up one dir, main page]

Skip to content

Commit 771f2c6

Browse files
authored
[RESTEASY-2728] Clients running in a resource method throw safer WebApplicationExceptions (#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.
1 parent 3b2dc7e commit 771f2c6

File tree

34 files changed

+3183
-17
lines changed

34 files changed

+3183
-17
lines changed

docbook/reference/en/en-US/modules/ExceptionMappers.xml

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,106 @@ If there is an ExceptionMapper for wrapped exception, then that is used to handl
131131
</tbody>
132132
</tgroup>
133133
</table>
134+
</sect1>
135+
<sect1 id="ResteasyWebApplicationException">
136+
<title>Resteasy WebApplicationExceptions</title>
137+
<para>Suppose a client at local.com calls the following resource method:</para>
138+
<programlisting>
139+
@GET
140+
@Path("remote")
141+
public String remote() throws Exception {
142+
Client client = ClientBuilder.newClient();
143+
return client.target("http://third.party.com/exception").request().get(String.class);
144+
}
145+
</programlisting>
146+
<para>If the call to http://third.party.com returns a status code 3xx, 4xx, or 5xx, then the
147+
<classname>Client</classname> is obliged by the JAX-RS
148+
specification to throw a <classname>WebApplicationException</classname>. Moreover, if the
149+
<classname>WebApplicationException</classname> contains a <classname>Response</classname>, which
150+
it normally would in RESTEasy, the server runtime is obliged by the JAX-RS specification to return that
151+
<classname>Response</classname>.
152+
As a result, information from the server at third.party.com, e.g., headers and body, will get sent back to
153+
local.com. The problem is that that information could be, at best, meaningless to the client
154+
and, at worst, a security breach.</para>
155+
156+
<para>RESTEasy has a solution that works around the problem and still conforms to the JAX-RS specification.
157+
In particular, for each <classname>WebApplicationException</classname> it defines a new subclass:</para>
158+
159+
<programlisting>
160+
WebApplicationException
161+
+-ResteasyWebApplicationException
162+
+-ClientErrorException
163+
| +-ResteasyClientErrorException
164+
| +-BadRequestException
165+
| | +-ResteasyBadRequestException
166+
| +-ForbiddenException
167+
| | +-ResteasyForbiddenException
168+
| +-NotAcceptableException
169+
| | +-ResteasyNotAcceptableException
170+
| +-NotAllowedException
171+
| | +-ResteasyNotAllowedException
172+
| +-NotAuthorizedException
173+
| | +-ResteasyNotAuthorizedException
174+
| +-NotFoundException
175+
| | +-ResteasyNotFoundException
176+
| +-NotSupportedException
177+
| | +-ResteasyNotSupportedException
178+
+-RedirectionException
179+
| +-ResteasyRedirectionException
180+
+-ServerErrorException
181+
| +-ResteasyServerErrorException
182+
| +-InternalServerErrorException
183+
| | +-ResteasyInternalServerErrorException
184+
| +-ServiceUnavailableException
185+
| | +-ResteasyServiceUnavailableException
186+
</programlisting>
187+
188+
<para>The new <classname>Exception</classname>s play the same role as the original ones,
189+
but RESTEasy treats them slightly dif 10000 ferently. When a <classname>Client</classname> detects
190+
that it is running in the context of a resource method, it will throw one of the new
191+
<classname>Exception</classname>s. However, instead of storing the original <classname>Response</classname>,
192+
it stores a "sanitized" version of the <classname>Response</classname>, in which only the status and
193+
the Allow and Content-Type headers are preserved. The original <classname>WebApplicationException</classname>,
194+
and therefore the original <classname>Response</classname>, can be accessed in one of two ways:</para>
195+
196+
<programlisting>
197+
// Create a NotAcceptableException.
198+
NotAcceptableException nae = new NotAcceptableException(Response.status(406).entity("ooops").build());
199+
200+
// Wrap the NotAcceptableException in a ResteasyNotAcceptableException.
201+
ResteasyNotAcceptableException rnae = (ResteasyNotAcceptableException) WebApplicationExceptionWrapper.wrap(nae);
202+
203+
// Extract the original NotAcceptableException using instance method.
204+
NotAcceptableException nae2 = rnae.unwrap();
205+
Assert.assertEquals(nae, nae2);
206+
207+
// Extract the original NotAcceptableException using class method.
208+
NotAcceptableException nae3 = (NotAcceptableException) WebApplicationExceptionWrapper.unwrap(nae); // second way
209+
Assert.assertEquals(nae, nae3);
210+
</programlisting>
211+
212+
<para>Note that this change is intended to introduce a safe default behavior in the case that
213+
the <classname>Exception</classname> generated by the remote call is allowed to make its way up
214+
to the server runtime. It is considered a good practice, though, to catch the
215+
<classname>Exception</classname> and treat it in some appropriate manner:</para>
216+
217+
<programlisting>
218+
@GET
219+
@Path("remote/{i}")
220+
public String remote(@PathParam("i") String i) throws Exception {
221+
Client client = ClientBuilder.newClient();
222+
try {
223+
return client.target("http://remote.com/exception/" + i).request().get(String.class);
224+
} catch (WebApplicationException wae) {
225+
...
226+
}
227+
}
228+
</programlisting>
229+
230+
<para><emphasis role="bold">Note.</emphasis> While RESTEasy will default to the new, safer behavior, the original behavior can
231+
be restored by setting the configuration parameter "resteasy.original.webapplicationexception.behavior"
232+
to "true".</para>
233+
134234
</sect1>
135235
<sect1 id="overring_resteasy_exceptions">
136236
<title>Overriding RESTEasy Builtin Exceptions</title>

docbook/reference/en/en-US/modules/Installation_Configuration.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,21 @@ String s = config.getOptionalValue("prop_name", String.class).orElse("d'oh");
10621062
will not occur.
10631063
</entry>
10641064
</row>
1065+
<row>
1066+
<entry>
1067+
resteasy.original.webapplicationexception.behavior
1068+
</entry>
1069+
<entry>
1070+
false
1071+
</entry>
1072+
<entry>
1073+
When set to "true", this parameter will restore the original behavior in which
1074+
a Client running in a resource method will throw a JAX-RS WebApplicationException
1075+
instead of a ResteasyWebApplicationException. See section
1076+
<link linkend='ResteasyWebApplicationException'>ResteasyWebApplicationException</link>
1077+
for more information.
1078+
</entry>
1079+
</row>
10651080
</tbody>
10661081
</tgroup>
10671082
</table>

docbook/reference/en/en-US/modules/MicroProfile_Rest_Client.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,13 @@ of a registered <classname>ResponseExceptionMapper</classname>, a default <class
412412
will map any response with status >= 400 to a <classname>WebApplicationException</classname>.
413413
</para>
414414

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

417424
<sect1>

resteasy-client-microprofile/src/main/java/org/jboss/resteasy/microprofile/client/DefaultResponseExceptionMapper.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package org.jboss.resteasy.microprofile.client;
22

33
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
4+
import org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper;
5+
import org.jboss.resteasy.core.Dispatcher;
6+
import org.jboss.resteasy.microprofile.config.ResteasyConfig;
7+
import org.jboss.resteasy.microprofile.config.ResteasyConfigFactory;
8+
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
9+
import org.jboss.resteasy.spi.ResteasyProviderFactory;
410

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

1925
@Override
2026
public boolean handles(int status, MultivaluedMap headers) {
21-
return status >= 400;
27+
final ResteasyConfig config = ResteasyConfigFactory.getConfig();
28+
final boolean originalBehavior = Boolean.parseBoolean(config.getValue(ResteasyContextParameters.RESTEASY_ORIGINAL_WEBAPPLICATIONEXCEPTION_BEHAVIOR,
29+
ResteasyConfig.SOURCE.SERVLET_CONTEXT, "false"));
30+
final boolean serverSide = ResteasyProviderFactory.searchContextData(Dispatcher.class) != null;
31+
return status >= (originalBehavior || !serverSide ? 400 : 300);
2232
}
2333

2434
@Override

resteasy-client/src/main/java/org/jboss/resteasy/client/jaxrs/internal/ClientInvocation.java

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import javax.ws.rs.ext.Providers;
4545
import javax.ws.rs.ext.WriterInterceptor;
4646

47+
import org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper;
4748
import org.jboss.resteasy.client.jaxrs.AsyncClientHttpEngine;
4849
import org.jboss.resteasy.client.jaxrs.ClientHttpEngine;
4950
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
@@ -109,6 +110,7 @@ public ClientInvocation(final ResteasyClient client, final URI uri, final Client
109110
* @param <T> type
110111
* @return extracted result of type T
111112
*/
113+
@SuppressWarnings("unchecked")
112114
public static <T> T extractResult(GenericType<T> responseType, Response response, Annotation[] annotations)
113115
{
114116
int status = response.getStatus();
@@ -191,7 +193,9 @@ public static <T> T extractResult(GenericType<T> responseType, Response response
191193
}
192194
}
193195
if (status >= 300 && status < 400)
194-
throw new RedirectionException(response);
196+
{
197+
throw WebApplicationExceptionWrapper.wrap(new RedirectionException(response));
198+
}
195199

196200
return handleErrorStatus(response);
197201
}
@@ -217,33 +221,33 @@ public static <T> T handleErrorStatus(Response response)
217221
switch (status)
218222
{
219223
case 400 :
220-
throw new BadRequestException(response);
224+
throw WebApplicationExceptionWrapper.wrap(new BadRequestException(response));
221225
case 401 :
222-
throw new NotAuthorizedException(response);
226+
throw WebApplicationExceptionWrapper.wrap(new NotAuthorizedException(response));
223227
case 403 :
224-
throw new ForbiddenException(response);
228+
throw WebApplicationExceptionWrapper.wrap(new ForbiddenException(response));
225229
case 404 :
226-
throw new NotFoundException(response);
230+
throw WebApplicationExceptionWrapper.wrap(new NotFoundException(response));
227231
case 405 :
228-
throw new NotAllowedException(response);
232+
throw WebApplicationExceptionWrapper.wrap(new NotAllowedException(response));
229233
case 406 :
230-
throw new NotAcceptableException(response);
234+
throw WebApplicationExceptionWrapper.wrap(new NotAcceptableException(response));
231235
case 415 :
232-
throw new NotSupportedException(response);
236+
throw WebApplicationExceptionWrapper.wrap(new NotSupportedException(response));
233237
case 500 :
234-
throw new InternalServerErrorException(response);
238+
throw WebApplicationExceptionWrapper.wrap(new InternalServerErrorException(response));
235239
case 503 :
236-
throw new ServiceUnavailableException(response);
240+
throw WebApplicationExceptionWrapper.wrap(new ServiceUnavailableException(response));
237241
default :
238242
break;
239243
}
240244

241245
if (status >= 400 && status < 500)
242-
throw new ClientErrorException(response);
246+
throw WebApplicationExceptionWrapper.wrap(new ClientErrorException(response));
243247
if (status >= 500)
244-
throw new ServerErrorException(response);
248+
throw WebApplicationExceptionWrapper.wrap(new ServerErrorException(response));
245249

246-
throw new WebApplicationException(response);
250+
throw WebApplicationExceptionWrapper.wrap(new WebApplicationException(response));
247251
}
248252

249253
public ClientConfiguration getClientConfiguration()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.jboss.resteasy.client.exception;
2+
3+
import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;
4+
5+
import javax.ws.rs.BadRequestException;
6+
import javax.ws.rs.core.Response;
7+
8+
/**
9+
* Wraps a {@link BadRequestException} with a {@linkplain #sanitize(Response) sanitized} response.
10+
*
11+
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
12+
*/
13+
public class ResteasyBadRequestException extends BadRequestException implements WebApplicationExceptionWrapper<BadRequestException> {
14+
private final BadRequestException wrapped;
15+
16+
ResteasyBadRequestException(final BadRequestException wrapped) {
17+
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
18+
this.wrapped = wrapped;
19+
}
20+
21+
@Override
22+
public BadRequestException unwrap() {
23+
return wrapped;
24+
}
25+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.jboss.resteasy.client.exception;
2+
3+
4+
import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;
5+
6+
import javax.ws.rs.ClientErrorException;
7+
import javax.ws.rs.core.Response;
8+
9+
/**
10+
* Wraps a {@link ClientErrorException} with a {@linkplain #sanitize(Response) sanitized} response.
11+
*
12+
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
13+
*/
14+
public class ResteasyClientErrorException extends ClientErrorException implements WebApplicationExceptionWrapper<ClientErrorException> {
15+
16+
private final ClientErrorException wrapped;
17+
18+
ResteasyClientErrorException(final ClientErrorException wrapped) {
19+
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
20+
this.wrapped = wrapped;
21+
}
22+
23+
@Override
24+
public ClientErrorException unwrap() {
25+
return wrapped;
26+
}
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.jboss.resteasy.client.exception;
2+
3+
import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;
4+
5+
import javax.ws.rs.ForbiddenException;
6+
import javax.ws.rs.core.Response;
7+
8+
/**
9+
* Wraps a {@link ForbiddenException} with a {@linkplain #sanitize(Response) sanitized} response.
10+
*
11+
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
12+
*/
13+
public class ResteasyForbiddenException extends ForbiddenException implements WebApplicationExceptionWrapper<ForbiddenException> {
14+
private final ForbiddenException wrapped;
15+
16+
ResteasyForbiddenException(final ForbiddenException wrapped) {
17+
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
18+
this.wrapped = wrapped;
19+
}
20+
21+
@Override
22+
public ForbiddenException unwrap() {
23+
return wrapped;
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.jboss.resteasy.client.exception;
2+
3+
import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;
4+
5+
import javax.ws.rs.InternalServerErrorException;
6+
import javax.ws.rs.core.Response;
7+
8+
/**
9+
* Wraps a {@link InternalServerErrorException} with a {@linkplain #sanitize(Response) sanitized} response.
10+
*
11+
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
12+
*/
13+
public class ResteasyInternalServerErrorException extends InternalServerErrorException implements WebApplicationExceptionWrapper<InternalServerErrorException> {
14+
private final InternalServerErrorException wrapped;
15+
16+
ResteasyInternalServerErrorException(final InternalServerErrorException wrapped) {
17+
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
18+
this.wrapped = wrapped;
19+
}
20+
21+
@Override
22+
public InternalServerErrorException unwrap() {
23+
return wrapped;
24+
}
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.jboss.resteasy.client.exception;
2+
3+
import static org.jboss.resteasy.client.exception.WebApplicationExceptionWrapper.sanitize;
4+
5+
import javax.ws.rs.NotAcceptableException;
6+
import javax.ws.rs.core.Response;
7+
8+
/**
9+
* Wraps a {@link NotAcceptableException} with a {@linkplain #sanitize(Response) sanitized} response.
10+
*
11+
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
12+
*/
13+
public class ResteasyNotAcceptableException extends NotAcceptableException implements WebApplicationExceptionWrapper<NotAcceptableException> {
14+
private final NotAcceptableException wrapped;
15+
16+
ResteasyNotAcceptableException(final NotAcceptableException wrapped) {
17+
super(wrapped.getMessage(), sanitize(wrapped.getResponse()), wrapped.getCause());
18+
this.wrapped = wrapped;
19+
}
20+
21+
@Override
22+
public NotAcceptableException unwrap() {
23+
return wrapped;
24+
}
25+
}

0 commit comments

Comments
 (0)
0