Skip to content

Commit f88cdf9

Browse files
committed
fix(resteasy,rest): ensure request body is consumed to avoid hanging
1 parent a0a8950 commit f88cdf9

File tree

4 files changed

+212
-0
lines changed

4 files changed

+212
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.quarkus.resteasy.multipart;
2+
3+
import java.util.function.Supplier;
4+
5+
import jakarta.annotation.Priority;
6+
import jakarta.ws.rs.Consumes;
7+
import jakarta.ws.rs.FormParam;
8+
import jakarta.ws.rs.POST;
9+
import jakarta.ws.rs.Path;
10+
import jakarta.ws.rs.Priorities;
11+
import jakarta.ws.rs.Produces;
12+
import jakarta.ws.rs.container.ContainerRequestContext;
13+
import jakarta.ws.rs.container.ContainerRequestFilter;
14+
import jakarta.ws.rs.core.MediaType;
15+
import jakarta.ws.rs.ext.Provider;
16+
17+
import org.jboss.resteasy.annotations.providers.multipart.MultipartForm;
18+
import org.jboss.shrinkwrap.api.ShrinkWrap;
19+
import org.jboss.shrinkwrap.api.asset.StringAsset;
20+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.RegisterExtension;
23+
24+
import io.quarkus.test.QuarkusUnitTest;
25+
import io.restassured.RestAssured;
26+
27+
public class LargeMultipartPayloadTest {
28+
29+
@RegisterExtension
30+
static QuarkusUnitTest test = new QuarkusUnitTest()
31+
.setArchiveProducer(new Supplier<>() {
32+
@Override
33+
public JavaArchive get() {
34+
return ShrinkWrap.create(JavaArchive.class)
35+
.addAsResource(new StringAsset("""
36+
quarkus.http.limits.max-form-attribute-size=30M
37+
quarkus.http.limits.max-body-size=30M
38+
"""),
39+
"application.properties");
40+
}
41+
});
42+
43+
@Test
44+
public void testConnectionClosedOnException() {
45+
RestAssured
46+
.given()
47+
.multiPart("content", twentyMegaBytes())
48+
.post("/test/multipart")
49+
.then()
50+
.statusCode(500);
51+
}
52+
53+
private static String twentyMegaBytes() {
54+
return new String(new byte[20_000_000]);
55+
}
56+
57+
@Path("/test")
58+
public static class Resource {
59+
@POST
60+
@Path("/multipart")
61+
@Produces(MediaType.TEXT_PLAIN)
62+
@Consumes(MediaType.MULTIPART_FORM_DATA)
63+
public String postForm(@MultipartForm final FormBody ignored) {
64+
return "ignored";
65+
}
66+
}
67+
68+
public static class FormBody {
69+
70+
@FormParam("content")
71+
public String content;
72+
73+
}
74+
75+
@Priority(Priorities.AUTHENTICATION)
76+
@Provider
77+
public static class Filter implements ContainerRequestFilter {
78+
79+
@Override
80+
public void filter(ContainerRequestContext containerRequestContext) {
81+
throw new RuntimeException("Expected exception");
82+
}
83+
}
84+
85+
}

extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/VertxHttpResponse.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.jboss.resteasy.spi.ResteasyProviderFactory;
1616

1717
import io.netty.buffer.ByteBuf;
18+
import io.vertx.core.AsyncResult;
19+
import io.vertx.core.Handler;
1820
import io.vertx.core.MultiMap;
1921
import io.vertx.core.http.HttpHeaders;
2022
import io.vertx.core.http.HttpMethod;
@@ -46,6 +48,15 @@ public VertxHttpResponse(HttpServerRequest request, ResteasyProviderFactory prov
4648
this.response = request.response();
4749
this.providerFactory = providerFactory;
4850
this.output = output;
51+
routingContext.addEndHandler(new Handler<AsyncResult<Void>>() {
52+
@Override
53+
public void handle(AsyncResult<Void> voidAsyncResult) {
54+
if (!routingContext.request().isEnded()) {
55+
// if we don't consume body, things can get stuck
56+
routingContext.request().resume();
57+
}
58+
}
59+
});
4960
}
5061

5162
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package io.quarkus.resteasy.reactive.server.test.multipart;
2+
3+
import static io.restassured.RestAssured.given;
4+
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.function.Supplier;
8+
9+
import jakarta.ws.rs.Consumes;
10+
import jakarta.ws.rs.POST;
11+
import jakarta.ws.rs.Path;
12+
import jakarta.ws.rs.core.MediaType;
13+
import jakarta.ws.rs.core.Response;
14+
15+
import org.hamcrest.Matchers;
16+
import org.jboss.jandex.ClassInfo;
17+
import org.jboss.jandex.MethodInfo;
18+
import org.jboss.resteasy.reactive.RestForm;
19+
import org.jboss.resteasy.reactive.common.model.ResourceClass;
20+
import org.jboss.resteasy.reactive.multipart.FileUpload;
21+
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
22+
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
23+
import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer;
24+
import org.jboss.resteasy.reactive.server.model.ServerResourceMethod;
25+
import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner;
26+
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;
27+
import org.jboss.shrinkwrap.api.ShrinkWrap;
28+
import org.jboss.shrinkwrap.api.asset.StringAsset;
29+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
30+
import org.junit.jupiter.api.Test;
31+
import org.junit.jupiter.api.extension.RegisterExtension;
32+
33+
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
34+
import io.quarkus.test.QuarkusUnitTest;
35+
import io.smallrye.mutiny.Uni;
36+
37+
public class LargeMultipartPayloadTest {
38+
39+
@RegisterExtension
40+
static QuarkusUnitTest test = new QuarkusUnitTest()
41+
.setArchiveProducer(new Supplier<>() {
42+
@Override
43+
public JavaArchive get() {
44+
return ShrinkWrap.create(JavaArchive.class)
45+
.addAsResource(new StringAsset("""
46+
quarkus.http.limits.max-form-attribute-size=30M
47+
quarkus.http.limits.max-body-size=30M
48+
"""),
49+
"application.properties");
50+
}
51+
}).addBuildChainCustomizer(buildChainBuilder -> buildChainBuilder.addBuildStep(context -> context.produce(
52+
new MethodScannerBuildItem(new MethodScanner() {
53+
@Override
54+
public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndpointClass,
55+
Map<String, Object> methodContext) {
56+
return List.of(new AlwaysFailHandler());
57+
}
58+
}))).produces(MethodScannerBuildItem.class).build());
59+
60+
@Test
61+
public void testConnectionClosedOnException() {
62+
given()
63+
.multiPart("file", twentyMegaBytes())
64+
.post("/test")
65+
.then()
66+
.statusCode(200)
67+
.body(Matchers.is("Expected failure!"));
68+
}
69+
70+
private static String twentyMegaBytes() {
71+
return new String(new byte[20_000_000]);
72+
}
73+
74+
@Path("/test")
75+
public static class Resource {
76+
77+
@POST
78+
@Consumes(MediaType.MULTIPART_FORM_DATA)
79+
public String uploadFile(@RestForm("file") FileUpload file) {
80+
return "File " + file.fileName() + " uploaded!";
81+
}
82+
}
83+
84+
public static class Mappers {
85+
86+
@ServerExceptionMapper(RuntimeException.class)
87+
Uni<Response> handle() {
88+
return Uni.createFrom().item(Response.status(200).entity("Expected failure!").build());
89+
}
90+
91+
}
92+
93+
public static class AlwaysFailHandler implements ServerRestHandler, HandlerChainCustomizer {
94+
95+
@Override
96+
public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
97+
requestContext.suspend();
98+
requestContext.resume(new RuntimeException("Expected exception!"), true);
99+
}
100+
101+
@Override
102+
public List<ServerRestHandler> handlers(Phase phase, ResourceClass resourceClass, ServerResourceMethod resourceMethod) {
103+
return List.of(new AlwaysFailHandler());
104+
}
105+
}
106+
}

independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/VertxResteasyReactiveRequestContext.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ public void handle(Void unused) {
8787
}
8888
};
8989
request.pause();
90+
91+
context.addEndHandler(new Handler<AsyncResult<Void>>() {
92+
@Override
93+
public void handle(AsyncResult<Void> voidAsyncResult) {
94+
if (!context.request().isEnded()) {
95+
// if we don't consume body, things can get stuck
96+
context.request().resume();
97+
}
98+
}
99+
});
90100
}
91101

92102
@Override

0 commit comments

Comments
 (0)