diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/devmode/QuarkusDefaultExceptionHandlingTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/devmode/QuarkusDefaultExceptionHandlingTest.java new file mode 100644 index 0000000000000..194daf36bc05d --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/devmode/QuarkusDefaultExceptionHandlingTest.java @@ -0,0 +1,64 @@ +package io.quarkus.resteasy.reactive.server.test.devmode; + +import static org.hamcrest.CoreMatchers.containsString; + +import java.util.function.Supplier; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class QuarkusDefaultExceptionHandlingTest { + + @RegisterExtension + static QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class); + } + + }); + + @Test + public void testDefaultErrorHandler() { + RestAssured.given().accept("text/html") + .get("/test/exception") + .then() + .statusCode(500) + .body(containsString("Internal Server Error"), containsString("dummy exception")); + } + + @Test + public void testNotFoundErrorHandler() { + RestAssured.given().accept("text/html") + .get("/test/exception2") + .then() + .statusCode(404) + .body(containsString("404 - Resource Not Found")); + } + + @Path("test") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.TEXT_PLAIN) + public static class Resource { + + @Path("exception") + @GET + @Produces("text/html") + public String exception() { + throw new RuntimeException("dummy exception"); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/devmode/UserProvidedExceptionHandlingTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/devmode/UserProvidedExceptionHandlingTest.java new file mode 100644 index 0000000000000..94091608508be --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/devmode/UserProvidedExceptionHandlingTest.java @@ -0,0 +1,72 @@ +package io.quarkus.resteasy.reactive.server.test.devmode; + +import java.util.function.Supplier; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class UserProvidedExceptionHandlingTest { + + @RegisterExtension + static QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class, CustomExceptionMapper.class); + } + + }); + + @Test + public void testDefaultErrorHandler() { + RestAssured.given().accept("text/html") + .get("/test/exception") + .then() + .statusCode(999); + } + + @Test + public void testNotFoundErrorHandler() { + RestAssured.given().accept("text/html") + .get("/test/exception2") + .then() + .statusCode(999); + } + + @Path("test") + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.TEXT_PLAIN) + public static class Resource { + + @Path("exception") + @GET + @Produces("text/html") + public String exception() { + throw new RuntimeException("dummy exception"); + } + } + + @Provider + public static class CustomExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(Exception exception) { + return Response.status(999).build(); + } + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/NotFoundExceptionMapper.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/NotFoundExceptionMapper.java index 6c9b447a175f1..d0277c57592b9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/NotFoundExceptionMapper.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/NotFoundExceptionMapper.java @@ -34,6 +34,7 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.common.util.ServerMediaType; import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.jboss.resteasy.reactive.server.core.RuntimeExceptionMapper; import org.jboss.resteasy.reactive.server.core.request.ServerDrivenNegotiation; import org.jboss.resteasy.reactive.server.handlers.RestInitialHandler; import org.jboss.resteasy.reactive.server.mapping.RequestMapper; @@ -81,8 +82,12 @@ public MethodDescription(String method, String fullPath, String produces, String } } - @ServerExceptionMapper(value = NotFoundException.class, priority = Priorities.USER + 1) - public Response toResponse(HttpHeaders headers) { + // we don't use NotFoundExceptionMapper here because that would result in users not being able to provide their own catch-all exception mapper in dev-mode, see https://github.com/quarkusio/quarkus/issues/7883 + @ServerExceptionMapper(priority = Priorities.USER + 1) + public Response toResponse(Throwable t, HttpHeaders headers) { + if (!(t instanceof NotFoundException)) { + return RuntimeExceptionMapper.IGNORE_RESPONSE; + } if ((classMappers == null) || classMappers.isEmpty()) { return respond(headers); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerExceptionMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerExceptionMapper.java index b7c32871c1607..7a4689f9ca757 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerExceptionMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/ServerExceptionMapper.java @@ -15,17 +15,16 @@ /** * When used on a method, then an implementation of {@link jakarta.ws.rs.ext.ExceptionMapper} is generated * that calls the annotated method with the proper arguments. - * + *

* When the annotation is placed on a method that is not a JAX-RS Resource class, the method handles exceptions in global - * fashion - * (as do regular JAX-RS {@code ExceptionMapper} implementations). + * fashion (as do regular JAX-RS {@code ExceptionMapper} implementations). * However, when it is placed on a method of a JAX-RS Resource class, the method is only used to handle exceptions originating * from * that JAX-RS Resource class. * Methods in a JAX-RS class annotated with this annotation will be used first when determining how to handle a thrown * exception. * This means that these methods take precedence over the global {@link jakarta.ws.rs.ext.ExceptionMapper} classes. - * + *

* In addition to the exception being handled, an annotated method can also declare any of the following * parameters (in any order): *