Skip to content

Commit

Permalink
Populate the default content type even when using Response
Browse files Browse the repository at this point in the history
Even when we use Response, it's interesting to push the default content
type to the context as it might get used by ContainerResponseFilters.

If the Response actually overrides it, it's all taken care of.

Fixes #34839
  • Loading branch information
gsmet committed Oct 31, 2024
1 parent a60aca2 commit be55617
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
* <p>
* 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<String> acceptValues;
if (requestContext.isProducesChecked() ||
(acceptValues = (List<String>) 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;
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.jboss.resteasy.reactive.server.handlers;

import java.util.List;

import jakarta.ws.rs.NotAcceptableException;
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}.
* <p>
* 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<String> 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) {
return;
}

if (MediaTypeHelper.isUnsupportedWildcardSubtype(res)) { // spec says the acceptable wildcard subtypes are */* or application/*
throw new NotAcceptableException();
}

requestContext.setResponseContentType(res);
}
}

0 comments on commit be55617

Please sign in to comment.