I'm learning Jersey and JAX-RS 2.x via my project "shop". I want my client SDK to raise a ShopException
whenever the HTTP response is 4xx or 5xx. Here's what I've tried—registering a ClientResponseFilter
in the client builder:
target =
ClientBuilder.newBuilder()
.register(ShopApplication.newJacksonJsonProvider())
.register((ClientResponseFilter) (requestCtx, responseCtx) -> {
if (responseCtx instanceof ClientResponse) {
ClientResponse resp = (ClientResponse) responseCtx;
if (resp.getStatus() >= 400) {
ShopExceptionData data = resp.readEntity(ShopExceptionData.class);
throw new ShopException(resp.getStatus(), data);
}
}
})
.build()
.target(Main.BASE_URI.resolve("products"));
And the test looks like:
@Test
public void getProduct_invalidId() {
try {
target.path("123!").request(APPLICATION_JSON).get(Product.class);
fail("GET should raise an exception");
} catch (ShopException e) {
assertThat(e.getData().getErrorCode()).isEqualTo(ShopError.PRODUCT_ID_INVALID.code);
assertThat(e.getData().getErrorMessage()).isEqualTo(ShopError.PRODUCT_ID_INVALID.message);
}
}
The problem is, my customized ShopException
is caught by Jersey and wrapped into javax.ws.rs.ProcessingException
:
javax.ws.rs.ProcessingException
at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:287)
at org.glassfish.jersey.client.JerseyInvocation.lambda$invoke$1(JerseyInvocation.java:767)
at org.glassfish.jersey.internal.Errors.process(Errors.java:316)
at org.glassfish.jersey.internal.Errors.process(Errors.java:298)
at org.glassfish.jersey.internal.Errors.process(Errors.java:229)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:414)
at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:765)
at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:428)
at org.glassfish.jersey.client.JerseyInvocation$Builder.get(JerseyInvocation.java:324)
at io.mincong.shop.rest.ProductResourceIT.getProduct_invalidId(ProductResourceIT.java:64)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: io.mincong.shop.rest.ShopException
at io.mincong.shop.rest.ProductResourceIT.lambda$setUp$0(ProductResourceIT.java:42)
at org.glassfish.jersey.client.ClientFilteringStages$ResponseFilterStage.apply(ClientFilteringStages.java:133)
at org.glassfish.jersey.client.ClientFilteringStages$ResponseFilterStage.apply(ClientFilteringStages.java:121)
at org.glassfish.jersey.process.internal.Stages.process(Stages.java:171)
at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:283)
... 33 more
Is there workaround to avoid ProcessingException
, and make sure the thrown exception is ShopException
?
Note: This is a partial answer as I haven't figured it out for all cases yet.
If you look at the source for the JerseyInvocation
, you will see the method invoke(Class responseType)
, which is the method that is called when we make the request passing the class parameter that we want the response deserialized to. This is what you used here, passing the Product.class
target.path("123!").request(APPLICATION_JSON).get(Product.class);
Looking at the source for the invoke()
method, we can see
return requestScope.runInScope(new Producer<T>() {
@Override
public T call() throws ProcessingException {
try {
return translate(runtime.invoke(requestForCall(requestContext)), requestScope, responseType);
} catch (final ProcessingException ex) {
if (ex.getCause() instanceof WebApplicationException) {
throw (WebApplicationException) ex.getCause();
}
throw ex;
}
}
});
The translate
method is what wraps the exception in a ProcessingException
. If you look at the couple lines after the catch
, you should see our opportunity for a workaround. If the cause of the exception is a WebApplicationException
, then that exception will be thrown. So your workaround is to make the ShopException
extend WebApplicationException
.
Now I say this is only a partial answer because this does not work when you just want a Response
back from the request
Response res = target.path("123!").request(APPLICATION_JSON).get();
When you do this, then the invoke()
(no arguments) is called. It doesn't do the same thing as the previous invoke()
method does. So if you can figure this out, then you have your complete solution.
The other workaround is to just catch the ProcessingException
yourself and throw rethrow the cause. If you're making a SDK, then this will be all done unbeknownst to the user anyway.