javaquarkusresteasy

Quarkus `@ServerExceptionMapper`: unwrap and forward to next exception mapper


I have some HTTP endpoints that end up throwing WebApplicationExceptions. Those get mapped using a global exception mapper:

@ServerExceptionMapper
public Response unwrapWebApplicationException(WebApplicationException exception) {
    return Response
            .status(exception.getResponse().getStatus())
            .entity("Bla bla something happened, technical details: " + exception.getResponse().getEntity())
            .build();
}

Now, in some circumstances my WebApplicationException is wrapped in some other exception, for example a ArcUndeclaredThrowableException. A constructed example may look like this:

@Path("/hello")
public class GreetingResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        WebApplicationException webApplicationException = new WebApplicationException(Response.Status.CONFLICT);
        throw new ArcUndeclaredThrowableException(webApplicationException);
    }
}

My goal is to write an exception mapper that unwraps the ArcUndeclaredThrowableException, but instead of directly returning a result, hands the inner exception to the next exception mapper, if one exists.

I found that this can be done using jakarta.ws.rs.ext.Providers#getExceptionMapper(Class<T> type). I wrote the following utility:

/**
 * Attempts to unwrap the exception's cause (shallowly, not recursively) and invoke that exception's exception mapper instead.
 * If the exception has no cause, it is thrown as-is. If it has a cause but the cause has no exception mapper, the cause exception is thrown as-is.
 */
private static <T extends Throwable> Response unwrapCause(T exception, Providers providers) throws Throwable {
    var cause = exception.getCause();
    if (cause == null) {
        throw exception;
    }
    @SuppressWarnings("unchecked") Class<Throwable> exceptionClass = (Class<Throwable>) cause.getClass();
    ExceptionMapper<Throwable> exceptionMapper = providers.getExceptionMapper(exceptionClass);
    if (exceptionMapper == null) {
        throw cause;
    }
    return exceptionMapper.toResponse(cause);
}

Now in theory all I need is to declare my exception mapper and call unwrapCause() in it. However, I was unable to obtain a proper instance of Providers. The Quarkus documentation on REST states:

Your exception mapper may declare any of the following parameter types: [...] Any of the Context objects.

And within those context objects, Providers is listed. However, when I try this:

/**
 * If quarkus encounters a checked exception in some proxy code that bubbles up to some other code that does not declare that checked exception,
 * it needs to wrap the exception in a runtime exception for it to be valid Java. Quarkus uses the {@link ArcUndeclaredThrowableException} for this.
 * It is of no informational value, so let's just always unwrap it.
 */
@ServerExceptionMapper
public Response unwrapArcUndeclaredThrowableExceptions(ArcUndeclaredThrowableException exception, Providers providers) throws Throwable {
    return unwrapCause(exception, providers);
}

I get this error during a Quarkus build:

Caused by: java.lang.RuntimeException: Parameter 'providers' of method 'unwrapArcUndeclaredThrowableExceptions of class 'org.acme.ExceptionMappers' is not allowed
    at org.jboss.resteasy.reactive.server.processor.generation.exceptionmappers.ServerExceptionMapperGenerator.getTargetMethodParamsInfo(ServerExceptionMapperGenerator.java:607)
    at org.jboss.resteasy.reactive.server.processor.generation.exceptionmappers.ServerExceptionMapperGenerator.generateRRResponse(ServerExceptionMapperGenerator.java:478)
    at org.jboss.resteasy.reactive.server.processor.generation.exceptionmappers.ServerExceptionMapperGenerator.generateGlobalMapper(ServerExceptionMapperGenerator.java:313)
    at io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveScanningProcessor.handleCustomAnnotatedMethods(ResteasyReactiveScanningProcessor.java:410)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:849)
    at io.quarkus.builder.BuildContext.run(BuildContext.java:256)
    at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
    at java.base/java.lang.Thread.run(Thread.java:1583)
    at org.jboss.threads.JBossThread.run(JBossThread.java:501)

If I instead try to inject a Providers instance outside like this:

@Context
Providers providers;

/**
 * If quarkus encounters a checked exception in some proxy code that bubbles up to some other code that does not declare that checked exception,
 * it needs to wrap the exception in a runtime exception for it to be valid Java. Quarkus uses the {@link ArcUndeclaredThrowableException} for this.
 * It is of no informational value, so let's just always unwrap it.
 */
@ServerExceptionMapper
public Response unwrapArcUndeclaredThrowableExceptions(ArcUndeclaredThrowableException exception) throws Throwable {
    return unwrapCause(exception, providers);
}

I end up with this error:

java.lang.IllegalStateException: This should never be called
    at org.acme.ExceptionMappers$GeneratedExceptionHandlerFor$WebApplicationException$OfMethod$unwrapWebApplicationException.toResponse(Unknown Source)
    at org.acme.ExceptionMappers$GeneratedExceptionHandlerFor$WebApplicationException$OfMethod$unwrapWebApplicationException.toResponse(Unknown Source)
    at org.acme.ExceptionMappers.unwrapCause(ExceptionMappers.java:49)
    at org.acme.ExceptionMappers.unwrapArcUndeclaredThrowableExceptions(ExceptionMappers.java:32)
    at org.acme.ExceptionMappers$GeneratedExceptionHandlerFor$ArcUndeclaredThrowableException$OfMethod$unwrapArcUndeclaredThrowableExceptions.toResponse(Unknown Source)
    at org.acme.ExceptionMappers$GeneratedExceptionHandlerFor$ArcUndeclaredThrowableException$OfMethod$unwrapArcUndeclaredThrowableExceptions.toResponse(Unknown Source)
    at org.jboss.resteasy.reactive.server.core.RuntimeExceptionMapper.mapException(RuntimeExceptionMapper.java:98)
    at org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext.mapExceptionIfPresent(ResteasyReactiveRequestContext.java:346)
    at org.jboss.resteasy.reactive.server.handlers.ExceptionHandler.handle(ExceptionHandler.java:15)
    at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:150)
    at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
    at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:599)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:1583)

How can I write an exception mapper that just unwraps a certain exception, having the cause go through any other potentially defined exception mappers?


Solution

  • You don't need to handle the unwrapping of the exception at all, Quarkus handles that for you (if the original exception is not being handled).

    So all you have to write is:

    @ServerExceptionMapper
    public Response unwrapWebApplicationException(WebApplicationException exception) {
        return Response
                .status(exception.getResponse().getStatus())
                .entity("Bla bla something happened, technical details: " + exception.getResponse().getEntity())
                .build();
    }