diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/ResourceLocatorTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/ResourceLocatorTest.java index 2f610a82cce42..a34cd901f3f3a 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/ResourceLocatorTest.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/ResourceLocatorTest.java @@ -5,6 +5,7 @@ import java.util.function.Supplier; +import io.quarkus.resteasy.reactive.server.test.resource.basic.resource.CorsPreflightResource; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Entity; @@ -37,6 +38,13 @@ import io.quarkus.resteasy.reactive.server.test.resource.basic.resource.ResourceLocatorSubresource3Interface; import io.quarkus.resteasy.reactive.server.test.simple.PortProviderUtil; import io.quarkus.test.QuarkusUnitTest; +import jakarta.ws.rs.HttpMethod; + +import java.util.Arrays; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; /** * @tpSubChapter Resource @@ -68,7 +76,7 @@ public JavaArchive get() { JavaArchive war = ShrinkWrap.create(JavaArchive.class); war.addClass(ResourceLocatorQueueReceiver.class).addClass(ResourceLocatorReceiver.class) .addClass(ResourceLocatorRootInterface.class).addClass(ResourceLocatorSubInterface.class) - .addClass(ResourceLocatorSubresource3Interface.class); + .addClass(ResourceLocatorSubresource3Interface.class).addClass(CorsPreflightResource.class); war.addClasses(PortProviderUtil.class, ResourceLocatorAbstractAnnotationFreeResource.class, ResourceLocatorAnnotationFreeSubResource.class, ResourceLocatorBaseResource.class, ResourceLocatorCollectionResource.class, ResourceLocatorDirectory.class, @@ -114,6 +122,48 @@ public void testSubresource() throws Exception { } } + /** + * @tpTestDetails Return custom handler for HTTP OPTIONS method in subresource redirection. The + * {@link CorsPreflightResource} instance should be returned + */ + @Test + @DisplayName("Test custom HTTP OPTIONS handler in subresource") + public void testOptionsMethodInSubresource() { + try (Response response = client.target(generateURL("/sub3/something/resources/test-options-method")).request() + .options()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + + var customHeader = response.getHeaderString(CorsPreflightResource.TEST_PREFLIGHT_HEADER); + assertThat(customHeader, notNullValue()); + assertThat(customHeader, is("test")); + + var allowHeader = response.getHeaderString("Allow"); + assertThat(allowHeader, notNullValue()); + assertThat(Arrays.asList(allowHeader.split(", ")), + containsInAnyOrder(HttpMethod.GET, HttpMethod.POST, HttpMethod.OPTIONS, HttpMethod.HEAD)); + } + } + + /** + * @tpTestDetails Custom handler for HTTP OPTIONS method in subresource. + */ + @Test + @DisplayName("Test custom explicit HTTP OPTIONS handler in subresource") + public void testOptionsMethodExplicitInSubresource() { + try (Response response = client.target(generateURL("/sub3/something/resources/test-options-method-explicit")).request() + .options()) { + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + + var customHeader = response.getHeaderString(ResourceLocatorSubresource2.TEST_PREFLIGHT_HEADER); + assertThat(customHeader, notNullValue()); + assertThat(customHeader, is("test")); + + var allowHeader = response.getHeaderString("Allow"); + assertThat(allowHeader, notNullValue()); + assertThat(Arrays.asList(allowHeader.split(", ")), containsInAnyOrder(HttpMethod.GET)); + } + } + /** * @tpTestDetails Two matching methods, one a resource locator, the other a resource method. * @tpSince RESTEasy 3.0.20 diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/CorsPreflightResource.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/CorsPreflightResource.java new file mode 100644 index 0000000000000..d1b56b49d77fa --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/CorsPreflightResource.java @@ -0,0 +1,17 @@ +package io.quarkus.resteasy.reactive.server.test.resource.basic.resource; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.OPTIONS; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +public class CorsPreflightResource { + public static final String TEST_PREFLIGHT_HEADER = "preflight-header-test"; + + @Path("{any:.*}") + @OPTIONS + public Response preflight() { + return Response.ok().allow(HttpMethod.GET, HttpMethod.POST, HttpMethod.OPTIONS, HttpMethod.HEAD) + .header(TEST_PREFLIGHT_HEADER, "test").build(); + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorBaseResource.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorBaseResource.java index 308a4a738be87..ab09e88f1643c 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorBaseResource.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorBaseResource.java @@ -6,6 +6,7 @@ import java.util.List; import jakarta.ws.rs.GET; +import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; @@ -61,4 +62,10 @@ public ResourceLocatorSubresource getSubresource() { return new ResourceLocatorSubresource(); } + @OPTIONS + @Path("{any:.*}") + public Object preflight() { + return "Here might be a custom handler for HTTP OPTIONS method"; + } + } diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorSubresource.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorSubresource.java index dfb0d717642ef..2ce81ce512ec5 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorSubresource.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorSubresource.java @@ -2,8 +2,10 @@ import java.util.List; +import io.vertx.core.http.HttpServerRequest; import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.Path; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; @@ -62,6 +64,20 @@ public String getValueFromBeanParam(@BeanParam Params params) { return params.param + " and " + params.value; } + @Path("/test-options-method-explicit") + public Object testOptionsMethodExplicit() { + return new ResourceLocatorSubresource2(); + } + + @Path("/test-options-method") + public Object testOptionsMethod(@Context HttpServerRequest request) { + if (request.method().name().equals(HttpMethod.OPTIONS)) { + return new CorsPreflightResource(); + } + + return "Should be used only with HTTP @OPTIONS method"; + } + public static class Params { @RestPath String param; diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorSubresource2.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorSubresource2.java index da745c63579ec..677ba369a33ec 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorSubresource2.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/ResourceLocatorSubresource2.java @@ -1,16 +1,19 @@ package io.quarkus.resteasy.reactive.server.test.resource.basic.resource; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; import org.jboss.logging.Logger; import org.junit.jupiter.api.Assertions; public class ResourceLocatorSubresource2 { - + public static final String TEST_PREFLIGHT_HEADER = "test-preflight-header"; private static final Logger LOG = Logger.getLogger(ResourceLocatorSubresource2.class); @GET @@ -35,4 +38,10 @@ public String doGet(@PathParam("param") String param, @Context UriInfo uri) { Assertions.assertEquals("2", param); return this.getClass().getName() + "-" + param; } + + @OPTIONS + @Path("{any:.*}") + public Response preflight() { + return Response.ok().allow(HttpMethod.GET).header(TEST_PREFLIGHT_HEADER, "test").build(); + } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceLocatorHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceLocatorHandler.java index 83215887b91af..9dafe4e57b544 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceLocatorHandler.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/ResourceLocatorHandler.java @@ -63,29 +63,29 @@ public void onComplete(Throwable throwable) { RequestMapper mapper = target.get(requestContext.getMethod()); boolean hadNullMethodMapper = false; if (mapper == null) { - String requestMethod = requestContext.getMethod(); - if (requestMethod.equals(HttpMethod.HEAD)) { - mapper = target.get(HttpMethod.GET); - } else if (requestMethod.equals(HttpMethod.OPTIONS)) { - Set allowedMethods = new HashSet<>(); - for (String method : target.keySet()) { - if (method == null) { - continue; - } - allowedMethods.add(method); - } - allowedMethods.add(HttpMethod.OPTIONS); - allowedMethods.add(HttpMethod.HEAD); - requestContext.abortWith(Response.ok().allow(allowedMethods).build()); - return; - } + mapper = target.get(null); //another layer of resource locators maybe + // we set this without checking if we matched, but we only use it after + // we check for a null mapper, so by the time we use it, it must have meant that + // we had a matcher for a null method + hadNullMethodMapper = true; if (mapper == null) { - mapper = target.get(null); //another layer of resource locators maybe - // we set this without checking if we matched, but we only use it after - // we check for a null mapper, so by the time we use it, it must have meant that - // we had a matcher for a null method - hadNullMethodMapper = true; + String requestMethod = requestContext.getMethod(); + if (requestMethod.equals(HttpMethod.HEAD)) { + mapper = target.get(HttpMethod.GET); + } else if (requestMethod.equals(HttpMethod.OPTIONS)) { + Set allowedMethods = new HashSet<>(); + for (String method : target.keySet()) { + if (method == null) { + continue; + } + allowedMethods.add(method); + } + allowedMethods.add(HttpMethod.OPTIONS); + allowedMethods.add(HttpMethod.HEAD); + requestContext.abortWith(Response.ok().allow(allowedMethods).build()); + return; + } } } if (mapper == null) {