diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeNoResolutionTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeNoResolutionTest.java new file mode 100644 index 0000000000000..c1b1e1e2f0e87 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeNoResolutionTest.java @@ -0,0 +1,60 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.io.IOException; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ContainerResponseFilterContentTypeNoResolutionTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot( + jar -> jar.addClasses(MediaTypeContainerResponseFilter.class, TestResource.class)); + + @Test + public void producesMediaTypePresentInWriterInterceptor() { + RestAssured + .given().accept("text/*") + .when().get("/test").then().statusCode(406); + } + + @Path("/test") + public static class TestResource { + + @GET + @Produces("text/*") + public Response hello() { + Greeting greeting = new Greeting("Hello"); + return Response.ok(greeting).build(); + } + } + + public record Greeting(String message) { + } + + @Priority(5000) + @Provider + public static class MediaTypeContainerResponseFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + if (responseContext.getMediaType() != null) { + throw new IllegalStateException( + "MediaType shouldn't have been resolved but got: " + responseContext.getMediaType()); + } + } + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeOverrideTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeOverrideTest.java new file mode 100644 index 0000000000000..627246b0e45d1 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeOverrideTest.java @@ -0,0 +1,61 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.io.IOException; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ContainerResponseFilterContentTypeOverrideTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot( + jar -> jar.addClasses(MediaTypeContainerResponseFilter.class, TestResource.class)); + + @Test + public void producesMediaTypePresentInWriterInterceptor() { + RestAssured.when().get("/test").then().body(Matchers.containsString("Hello")); + } + + @Path("/test") + public static class TestResource { + + @GET + @Produces(MediaType.TEXT_XML) + public Response hello() { + Greeting greeting = new Greeting("Hello"); + return Response.ok(greeting).type(MediaType.TEXT_PLAIN).build(); + } + } + + public record Greeting(String message) { + } + + @Priority(5000) + @Provider + public static class MediaTypeContainerResponseFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + if (!responseContext.getMediaType().isCompatible(MediaType.TEXT_PLAIN_TYPE)) { + throw new IllegalStateException( + "MediaType was not overridden by Response, got: " + responseContext.getMediaType() + + " instead of expected: " + MediaType.TEXT_PLAIN); + } + } + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeTest.java new file mode 100644 index 0000000000000..7c290278784e4 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeTest.java @@ -0,0 +1,60 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.io.IOException; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ContainerResponseFilterContentTypeTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot( + jar -> jar.addClasses(MediaTypeContainerResponseFilter.class, TestResource.class)); + + @Test + public void producesMediaTypePresentInWriterInterceptor() { + RestAssured.when().get("/test").then().body(Matchers.containsString("Hello")); + } + + @Path("/test") + public static class TestResource { + + @GET + @Produces(MediaType.TEXT_XML) + public Response hello() { + Greeting greeting = new Greeting("Hello"); + return Response.ok(greeting).build(); + } + } + + public record Greeting(String message) { + } + + @Priority(5000) + @Provider + public static class MediaTypeContainerResponseFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + if (!responseContext.getMediaType().isCompatible(MediaType.TEXT_XML_TYPE)) { + throw new IllegalStateException("MediaType was not provided, got: " + responseContext.getMediaType() + + " instead of expected: " + MediaType.TEXT_XML); + } + } + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 2bc5999117537..ebb44b796a686 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -70,6 +70,7 @@ import org.jboss.resteasy.reactive.server.handlers.BlockingHandler; import org.jboss.resteasy.reactive.server.handlers.ExceptionHandler; import org.jboss.resteasy.reactive.server.handlers.FixedProducesHandler; +import org.jboss.resteasy.reactive.server.handlers.FixedProducesSetDefaultContentTypeResponseHandler; import org.jboss.resteasy.reactive.server.handlers.FormBodyHandler; import org.jboss.resteasy.reactive.server.handlers.InputHandler; import org.jboss.resteasy.reactive.server.handlers.InstanceHandler; @@ -85,6 +86,7 @@ import org.jboss.resteasy.reactive.server.handlers.ResponseWriterHandler; import org.jboss.resteasy.reactive.server.handlers.SseResponseWriterHandler; import org.jboss.resteasy.reactive.server.handlers.VariableProducesHandler; +import org.jboss.resteasy.reactive.server.handlers.VariableProducesSetDefaultContentTypeResponseHandler; import org.jboss.resteasy.reactive.server.mapping.RuntimeResource; import org.jboss.resteasy.reactive.server.mapping.URITemplate; import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; @@ -472,6 +474,18 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, : ScoreSystem.Diagnostic.WriterNotRequired); } } else { + if (method.getProduces() != null && method.getProduces().length > 0) { + if (method.getProduces().length == 1) { + MediaType mediaType = MediaType.valueOf(method.getProduces()[0]); + if (mediaType.isWildcardType() || mediaType.isWildcardSubtype()) { + handlers.add(new VariableProducesSetDefaultContentTypeResponseHandler(serverMediaType)); + } else { + handlers.add(new FixedProducesSetDefaultContentTypeResponseHandler(mediaType)); + } + } else { + handlers.add(new VariableProducesSetDefaultContentTypeResponseHandler(serverMediaType)); + } + } score.add(ScoreSystem.Category.Writer, ScoreSystem.Diagnostic.WriterRunTime); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesSetDefaultContentTypeResponseHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesSetDefaultContentTypeResponseHandler.java new file mode 100644 index 0000000000000..7f2ace85ffb6e --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesSetDefaultContentTypeResponseHandler.java @@ -0,0 +1,57 @@ +package org.jboss.resteasy.reactive.server.handlers; + +import java.util.List; +import java.util.Locale; + +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.server.core.EncodedMediaType; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +/** + * Handler that defines the default content type when a Response is returned. + * While it might not be the final content type, we still need to make sure + * the default content type is provided to {@code ContainerResponseFilter}. + *

+ * This particular one is for endpoints that only produce one content type. + */ +@SuppressWarnings("ForLoopReplaceableByForEach") +public class FixedProducesSetDefaultContentTypeResponseHandler implements ServerRestHandler { + + private final EncodedMediaType mediaType; + private final String mediaTypeString; + private final String mediaTypeSubstring; + + public FixedProducesSetDefaultContentTypeResponseHandler(MediaType mediaType) { + this.mediaType = new EncodedMediaType(mediaType); + this.mediaTypeString = mediaType.getType() + "/" + mediaType.getSubtype(); + this.mediaTypeSubstring = mediaType.getType() + "/*"; + } + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { + List acceptValues; + if (requestContext.isProducesChecked() || + (acceptValues = (List) requestContext.getHeader(HttpHeaders.ACCEPT, false)).isEmpty()) { + requestContext.setResponseContentType(mediaType); + } else { + for (int i = 0; i < acceptValues.size(); i++) { + String accept = acceptValues.get(i); + //TODO: this needs to be optimized + if (accept.contains(mediaTypeString) || accept.contains("*/*") || accept.contains(mediaTypeSubstring)) { + requestContext.setResponseContentType(mediaType); + break; + } else { + // some clients might be sending the header with incorrect casing... + String lowercaseAccept = accept.toLowerCase(Locale.ROOT); + if (lowercaseAccept.contains(mediaTypeString) || lowercaseAccept.contains(mediaTypeSubstring)) { + requestContext.setResponseContentType(mediaType); + break; + } + } + } + } + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesSetDefaultContentTypeResponseHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesSetDefaultContentTypeResponseHandler.java new file mode 100644 index 0000000000000..8143387e8d710 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/VariableProducesSetDefaultContentTypeResponseHandler.java @@ -0,0 +1,49 @@ +package org.jboss.resteasy.reactive.server.handlers; + +import java.util.List; + +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.common.util.MediaTypeHelper; +import org.jboss.resteasy.reactive.common.util.ServerMediaType; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +/** + * Handler that defines the default content type when a Response is returned. + * While it might not be the final content type, we still need to make sure + * the default content type is provided to {@code ContainerResponseFilter}. + *

+ * This particular one negotiates the content type when there are multiple ones defined. + */ +public class VariableProducesSetDefaultContentTypeResponseHandler implements ServerRestHandler { + + private final ServerMediaType mediaTypeList; + + public VariableProducesSetDefaultContentTypeResponseHandler(ServerMediaType mediaTypeList) { + this.mediaTypeList = mediaTypeList; + } + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { + MediaType res = null; + List accepts = requestContext.getHttpHeaders().getRequestHeader(HttpHeaders.ACCEPT); + for (String accept : accepts) { + res = mediaTypeList.negotiateProduces(accept).getKey(); + if (res != null) { + break; + } + } + if (res == null) { // fallback to ensure that MessageBodyWriter is passed the proper media type + res = mediaTypeList.negotiateProduces(requestContext.serverRequest().getRequestHeader(HttpHeaders.ACCEPT)) + .getKey(); + } + + if (res == null || MediaTypeHelper.isUnsupportedWildcardSubtype(res)) { + return; + } + + requestContext.setResponseContentType(res); + } +}