Search code examples
javajax-rsjersey-2.0jersey-client

How to raise customized exception in Jersey Client?


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?


Solution

  • 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.