2

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?

Mincong Huang
  • 5,284
  • 8
  • 39
  • 62

1 Answers1

1

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.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Thanks for this detailed explanation. Your 1st proposal, invoke the no-arg method doesn't work. In some moment, the exception becomes InvocationTargetException, then caught and mapped as `MappableException`. See https://github.com/jersey/jersey/blob/2.27/core-server/src/main/java/org/glassfish/jersey/server/model/internal/AbstractJavaResourceMethodDispatcher.java#L208-L209 and finally unwrapped and went back to ProcessingException. So I'll use the your 2nd proposal. I also noticed that there's a Jersey-Proxy-Client, perhaps it works too. – Mincong Huang May 30 '18 at 15:31