diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java index daef9193a55a0..d4345ce8cfd7b 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientConfig.java @@ -43,6 +43,8 @@ public class RestClientConfig { EMPTY.connectionPoolSize = Optional.empty(); EMPTY.keepAliveEnabled = Optional.empty(); EMPTY.maxRedirects = Optional.empty(); + EMPTY.multipart = new RestClientMultipartConfig(); + EMPTY.multipart.maxChunkSize = Optional.empty(); EMPTY.headers = Collections.emptyMap(); EMPTY.shared = Optional.empty(); EMPTY.name = Optional.empty(); @@ -51,6 +53,8 @@ public class RestClientConfig { EMPTY.alpn = Optional.empty(); } + public RestClientMultipartConfig multipart; + /** * The base URL to use for this service. This property or the `uri` property is considered required, unless * the `baseUri` attribute is configured in the `@RegisterRestClient` annotation. @@ -296,6 +300,9 @@ public static RestClientConfig load(String configKey) { instance.userAgent = getConfigValue(configKey, "user-agent", String.class); instance.http2 = getConfigValue(configKey, "http2", Boolean.class); + instance.multipart = new RestClientMultipartConfig(); + instance.multipart.maxChunkSize = getConfigValue(configKey, "multipart.max-chunk-size", Integer.class); + return instance; } @@ -333,6 +340,9 @@ public static RestClientConfig load(Class interfaceClass) { instance.http2 = getConfigValue(interfaceClass, "http2", Boolean.class); instance.alpn = getConfigValue(interfaceClass, "alpn", Boolean.class); + instance.multipart = new RestClientMultipartConfig(); + instance.multipart.maxChunkSize = getConfigValue(interfaceClass, "multipart.max-chunk-size", Integer.class); + return instance; } diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientMultipartConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientMultipartConfig.java new file mode 100644 index 0000000000000..ca6f7fa662b31 --- /dev/null +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientMultipartConfig.java @@ -0,0 +1,19 @@ +package io.quarkus.restclient.config; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class RestClientMultipartConfig { + + /** + * The max HTTP chunk size (8096 bytes by default). + * + * This property is applicable to reactive REST clients only. + */ + @ConfigItem + public Optional maxChunkSize; + +} diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java index eed133d3ca4d0..4412f9a29c6e6 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/main/java/io/quarkus/restclient/config/RestClientsConfig.java @@ -102,6 +102,8 @@ public class RestClientsConfig { public RestClientLoggingConfig logging; + public RestClientMultipartConfig multipart; + /** * A timeout in milliseconds that REST clients should wait to connect to the remote endpoint. * diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java index ad43a18cbdb40..8ce5ca469dd37 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/test/java/io/quarkus/restclient/config/RestClientConfigTest.java @@ -72,6 +72,7 @@ private void verifyConfig(RestClientConfig config) { assertThat(config.connectionTTL.get()).isEqualTo(30000); assertThat(config.connectionPoolSize).isPresent(); assertThat(config.connectionPoolSize.get()).isEqualTo(10); + assertThat(config.multipart.maxChunkSize.get()).isEqualTo(1024); } private static void setupMPConfig() throws IOException { diff --git a/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties b/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties index e4f04634edd40..97477e0704f51 100644 --- a/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties +++ b/extensions/resteasy-classic/rest-client-config/runtime/src/test/resources/application.properties @@ -10,6 +10,7 @@ quarkus.rest-client.test-client.query-param-style=COMMA_SEPARATED quarkus.rest-client.test-client.hostname-verifier=io.quarkus.restclient.configuration.MyHostnameVerifier quarkus.rest-client.test-client.connection-ttl=30000 quarkus.rest-client.test-client.connection-pool-size=10 +quarkus.rest-client.test-client.multipart.max-chunk-size=1024 quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest".url=http://localhost:8080 quarkus.rest-client."RestClientConfigTest".uri=http://localhost:8081 @@ -23,3 +24,4 @@ quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest".query-pa quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest".hostname-verifier=io.quarkus.restclient.configuration.MyHostnameVerifier quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest".connection-ttl=30000 quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest".connection-pool-size=10 +quarkus.rest-client."io.quarkus.restclient.config.RestClientConfigTest".multipart.max-chunk-size=1024 diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultiByteFileTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultiByteFileTest.java index ee6e94b1544a8..4af50b19dd048 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultiByteFileTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/multipart/MultiByteFileTest.java @@ -18,6 +18,9 @@ import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.rest.client.RestClientBuilder; @@ -63,6 +66,21 @@ void shouldUploadBiggishFile() { assertThat(result).isEqualTo("myFile"); } + @Test + void shouldChunkRequest() { + Client client = RestClientBuilder.newBuilder() + .baseUri(baseUri) + .property("io.quarkus.rest.client.max-chunk-size", 2) + .build(Client.class); + + ClientForm form = new ClientForm(); + form.file = Multi.createBy().repeating().supplier( + () -> (byte) 'A').atMost(10); + + String result = client.chunked(form); + assertThat(result).isEqualTo("myFile/-1/chunked"); + } + @Test void shouldUploadTwoSmallFiles() { Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); @@ -180,6 +198,15 @@ public String uploadNull(@MultipartForm FormDataWithFile form) { return form.myFile != null ? "NON_NULL_FILE_FROM_NULL_MULTI" : "NULL_FILE"; } + @Path("/chunked") + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String chunked(@Context HttpHeaders headers, @MultipartForm FormData form) { + String filename = verifyFile(form.myFile, 10, b -> (byte) 'A'); + return filename + "/" + headers.getLength() + "/" + headers.getHeaderString("transfer-encoding"); + } + @Path("/") @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @@ -269,6 +296,11 @@ public interface Client { @Consumes(MediaType.MULTIPART_FORM_DATA) String postMultipart(@MultipartForm ClientForm clientForm); + @Path("/chunked") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + String chunked(@MultipartForm ClientForm clientForm); + @Path("/two-files") @POST @Consumes(MediaType.MULTIPART_FORM_DATA) diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/Constants.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/Constants.java new file mode 100644 index 0000000000000..2aa65ed8899e9 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/Constants.java @@ -0,0 +1,5 @@ +package io.quarkus.rest.client.reactive.runtime; + +public class Constants { + public final static int DEFAULT_MAX_CHUNK_SIZE = 8096; +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index c47832f4fd14c..680b9f0c89692 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -1,5 +1,7 @@ package io.quarkus.rest.client.reactive.runtime; +import static io.quarkus.rest.client.reactive.runtime.Constants.DEFAULT_MAX_CHUNK_SIZE; + import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; @@ -360,6 +362,13 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi clientBuilder.setUserAgent(restClientsConfig.userAgent.get()); } + Integer maxChunkSize = (Integer) getConfiguration().getProperty(QuarkusRestClientProperties.MAX_CHUNK_SIZE); + if (maxChunkSize != null) { + clientBuilder.maxChunkSize(maxChunkSize); + } else { + clientBuilder.maxChunkSize(DEFAULT_MAX_CHUNK_SIZE); + } + if (getConfiguration().hasProperty(QuarkusRestClientProperties.HTTP2)) { clientBuilder.http2((Boolean) getConfiguration().getProperty(QuarkusRestClientProperties.HTTP2)); } else if (restClientsConfig.http2) { diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java index 2173915ef0054..fe65068f5a98b 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilder.java @@ -1,5 +1,7 @@ package io.quarkus.rest.client.reactive.runtime; +import static io.quarkus.rest.client.reactive.runtime.Constants.DEFAULT_MAX_CHUNK_SIZE; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -120,6 +122,10 @@ private void configureCustomProperties(QuarkusRestClientBuilder builder) { builder.property(QuarkusRestClientProperties.USER_AGENT, userAgent.get()); } + Optional maxChunkSize = oneOf(clientConfigByClassName().multipart.maxChunkSize, + clientConfigByConfigKey().multipart.maxChunkSize, configRoot.multipart.maxChunkSize); + builder.property(QuarkusRestClientProperties.MAX_CHUNK_SIZE, maxChunkSize.orElse(DEFAULT_MAX_CHUNK_SIZE)); + Boolean http2 = oneOf(clientConfigByClassName().http2, clientConfigByConfigKey().http2).orElse(configRoot.http2); builder.property(QuarkusRestClientProperties.HTTP2, http2); diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java index 7a60af0b2d842..36556d81add90 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java @@ -29,6 +29,7 @@ import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder; import io.quarkus.restclient.config.RestClientConfig; +import io.quarkus.restclient.config.RestClientMultipartConfig; import io.quarkus.restclient.config.RestClientsConfig; @SuppressWarnings({ "SameParameterValue" }) @@ -103,6 +104,7 @@ public void testClientSpecificConfigs() { Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.CONNECTION_POOL_SIZE, 103); Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.KEEP_ALIVE_ENABLED, false); Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MAX_REDIRECTS, 104); + Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MAX_CHUNK_SIZE, 1024); Mockito.verify(restClientBuilderMock).followRedirects(true); Mockito.verify(restClientBuilderMock).register(MyResponseFilter1.class); Mockito.verify(restClientBuilderMock).queryParamStyle(QueryParamStyle.COMMA_SEPARATED); @@ -145,6 +147,7 @@ public void testGlobalConfigs() { Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.CONNECTION_POOL_SIZE, 203); Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.KEEP_ALIVE_ENABLED, true); Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MAX_REDIRECTS, 204); + Mockito.verify(restClientBuilderMock).property(QuarkusRestClientProperties.MAX_CHUNK_SIZE, 1024); Mockito.verify(restClientBuilderMock).followRedirects(true); Mockito.verify(restClientBuilderMock).register(MyResponseFilter2.class); Mockito.verify(restClientBuilderMock).queryParamStyle(QueryParamStyle.MULTI_PAIRS); @@ -173,6 +176,8 @@ private static RestClientsConfig createSampleConfigRoot() { configRoot.connectionPoolSize = Optional.of(203); configRoot.keepAliveEnabled = Optional.of(true); configRoot.maxRedirects = Optional.of(204); + configRoot.multipart = new RestClientMultipartConfig(); + configRoot.multipart.maxChunkSize = Optional.of(1024); configRoot.followRedirects = Optional.of(true); configRoot.providers = Optional .of("io.quarkus.rest.client.reactive.runtime.RestClientCDIDelegateBuilderTest$MyResponseFilter2"); @@ -211,6 +216,8 @@ private static RestClientConfig createSampleClientConfig() { clientConfig.keepAliveEnabled = Optional.of(false); clientConfig.maxRedirects = Optional.of(104); clientConfig.followRedirects = Optional.of(true); + clientConfig.multipart = new RestClientMultipartConfig(); + clientConfig.multipart.maxChunkSize = Optional.of(1024); clientConfig.providers = Optional .of("io.quarkus.rest.client.reactive.runtime.RestClientCDIDelegateBuilderTest$MyResponseFilter1"); clientConfig.queryParamStyle = Optional.of(QueryParamStyle.COMMA_SEPARATED); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java index c51ea17bc8746..667fa756bb392 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/api/QuarkusRestClientProperties.java @@ -2,6 +2,11 @@ public class QuarkusRestClientProperties { + /** + * Configures the maximum chunk size. + */ + public static final String MAX_CHUNK_SIZE = "io.quarkus.rest.client.max-chunk-size"; + /** * Configure the connect timeout in ms. */ diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java index 21d2bb60df41b..385da9940a9ed 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java @@ -70,9 +70,11 @@ public class ClientSendRequestHandler implements ClientRestHandler { private final LoggingScope loggingScope; private final ClientLogger clientLogger; private final Map, MultipartResponseData> multipartResponseDataMap; + private final int maxChunkSize; - public ClientSendRequestHandler(boolean followRedirects, LoggingScope loggingScope, ClientLogger logger, + public ClientSendRequestHandler(int maxChunkSize, boolean followRedirects, LoggingScope loggingScope, ClientLogger logger, Map, MultipartResponseData> multipartResponseDataMap) { + this.maxChunkSize = maxChunkSize; this.followRedirects = followRedirects; this.loggingScope = loggingScope; this.clientLogger = logger; @@ -457,7 +459,7 @@ private QuarkusMultipartFormUpload setMultipartHeadersAndPrepareBody(HttpClientR mode = (PausableHttpPostRequestEncoder.EncoderMode) property; } QuarkusMultipartFormUpload multipartFormUpload = new QuarkusMultipartFormUpload(Vertx.currentContext(), multipartForm, - true, mode); + true, maxChunkSize, mode); httpClientRequest.setChunked(multipartFormUpload.isChunked()); setEntityRelatedHeaders(headerMap, state.getEntity()); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java index 2c60d09f2b7ae..8cabb175d17ff 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java @@ -71,6 +71,7 @@ public class ClientBuilderImpl extends ClientBuilder { private LoggingScope loggingScope; private Integer loggingBodySize = 100; + private int maxChunkSize = 8096; private MultiQueryParamMode multiQueryParamMode; private ClientLogger clientLogger = new DefaultClientLogger(); @@ -196,6 +197,11 @@ public ClientBuilder enableCompression() { return this; } + public ClientBuilder maxChunkSize(int maxChunkSize) { + this.maxChunkSize = maxChunkSize; + return this; + } + @Override public ClientImpl build() { HttpClientOptions options = Optional.ofNullable(configuration.getFromContext(HttpClientOptions.class)) @@ -287,6 +293,7 @@ public ClientImpl build() { clientLogger.setBodySize(loggingBodySize); + options.setMaxChunkSize(maxChunkSize); return new ClientImpl(options, configuration, CLIENT_CONTEXT_RESOLVER.resolve(Thread.currentThread().getContextClassLoader()), diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java index d8123700fda59..87b523f8465e6 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java @@ -201,7 +201,8 @@ public Vertx get() { }); } - handlerChain = new HandlerChain(followRedirects, loggingScope, clientContext.getMultipartResponsesData(), clientLogger); + handlerChain = new HandlerChain(options.getMaxChunkSize(), followRedirects, loggingScope, + clientContext.getMultipartResponsesData(), clientLogger); } public ClientContext getClientContext() { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java index 5be02342cb8e9..7bfc14ec28135 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/HandlerChain.java @@ -36,11 +36,12 @@ class HandlerChain { private ClientRestHandler preClientSendHandler = null; - public HandlerChain(boolean followRedirects, LoggingScope loggingScope, + public HandlerChain(int maxChunkSize, boolean followRedirects, LoggingScope loggingScope, Map, MultipartResponseData> multipartData, ClientLogger clientLogger) { this.clientCaptureCurrentContextRestHandler = new ClientCaptureCurrentContextRestHandler(); this.clientSwitchToRequestContextRestHandler = new ClientSwitchToRequestContextRestHandler(); - this.clientSendHandler = new ClientSendRequestHandler(followRedirects, loggingScope, clientLogger, multipartData); + this.clientSendHandler = new ClientSendRequestHandler(maxChunkSize, followRedirects, loggingScope, clientLogger, + multipartData); this.clientSetResponseEntityRestHandler = new ClientSetResponseEntityRestHandler(); this.clientResponseCompleteRestHandler = new ClientResponseCompleteRestHandler(); this.clientErrorHandler = new ClientErrorHandler(loggingScope); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/PausableHttpPostRequestEncoder.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/PausableHttpPostRequestEncoder.java index 9d59b139f767b..cdbc132742add 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/PausableHttpPostRequestEncoder.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/PausableHttpPostRequestEncoder.java @@ -215,6 +215,11 @@ public enum EncoderMode { */ private boolean isChunked; + /** + * The max HTTP chunk + */ + private int maxChunkSize; + /** * InterfaceHttpData for Body (without encoding) */ @@ -263,8 +268,8 @@ public enum EncoderMode { * if the request is a TRACE */ public PausableHttpPostRequestEncoder( - HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset, - EncoderMode encoderMode) + HttpDataFactory factory, HttpRequest request, boolean multipart, int maxChunkSize, + Charset charset, EncoderMode encoderMode) throws ErrorDataEncoderException { this.request = checkNotNull(request, "request"); this.charset = checkNotNull(charset, "charset"); @@ -278,6 +283,7 @@ public PausableHttpPostRequestEncoder( isLastChunk = false; isLastChunkSent = false; isMultipart = multipart; + this.maxChunkSize = maxChunkSize; multipartHttpDatas = new ArrayList<>(); this.encoderMode = encoderMode; if (isMultipart) { @@ -812,7 +818,6 @@ public HttpRequest finalizeRequest() throws ErrorDataEncoderException { HttpHeaders headers = request.headers(); List contentTypes = headers.getAll(HttpHeaderNames.CONTENT_TYPE); - List transferEncoding = headers.getAll(HttpHeaderNames.TRANSFER_ENCODING); if (contentTypes != null) { headers.remove(HttpHeaderNames.CONTENT_TYPE); for (String contentType : contentTypes) { @@ -842,18 +847,9 @@ public HttpRequest finalizeRequest() throws ErrorDataEncoderException { iterator = multipartHttpDatas.listIterator(); headers.set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(realSize)); - if (realSize > QuarkusHttpPostBodyUtil.chunkSize || isMultipart) { + if (realSize > maxChunkSize) { isChunked = true; - if (transferEncoding != null) { - headers.remove(HttpHeaderNames.TRANSFER_ENCODING); - for (CharSequence v : transferEncoding) { - if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(v)) { - // ignore - } else { - headers.add(HttpHeaderNames.TRANSFER_ENCODING, v); - } - } - } + removeChunkedFromTransferEncoding(headers); // wrap to hide the possible content return new WrappedHttpRequest(request); @@ -863,11 +859,14 @@ public HttpRequest finalizeRequest() throws ErrorDataEncoderException { if (request instanceof FullHttpRequest) { FullHttpRequest fullRequest = (FullHttpRequest) request; ByteBuf chunkContent = chunk.content(); - if (fullRequest.content() != chunkContent) { + if (fullRequest.content() != chunkContent && chunkContent != null) { fullRequest.content().clear().writeBytes(chunkContent); chunkContent.release(); + } else if (chunkContent == null) { + isChunked = true; + removeChunkedFromTransferEncoding(headers); } - return fullRequest; + return new WrappedHttpRequest(fullRequest); } else { return new WrappedFullHttpRequest(request, chunk); } @@ -881,6 +880,21 @@ public boolean isChunked() { return isChunked; } + private void removeChunkedFromTransferEncoding(HttpHeaders headers) { + List transferEncoding = headers.getAll(HttpHeaderNames.TRANSFER_ENCODING); + + if (transferEncoding != null) { + headers.remove(HttpHeaderNames.TRANSFER_ENCODING); + for (CharSequence v : transferEncoding) { + if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(v)) { + // ignore + } else { + headers.add(HttpHeaderNames.TRANSFER_ENCODING, v); + } + } + } + } + /** * Encode one attribute * @@ -926,8 +940,8 @@ private String encodeAttribute(String s, Charset charset) throws ErrorDataEncode */ private ByteBuf fillByteBuf() { int length = currentBuffer.readableBytes(); - if (length > QuarkusHttpPostBodyUtil.chunkSize) { - return currentBuffer.readRetainedSlice(QuarkusHttpPostBodyUtil.chunkSize); + if (length > maxChunkSize) { + return currentBuffer.readRetainedSlice(maxChunkSize); } else { // to continue ByteBuf slice = currentBuffer; @@ -981,7 +995,7 @@ private HttpContent encodeNextChunkMultipart(int sizeleft) throws ErrorDataEncod } else { currentBuffer = wrappedBuffer(currentBuffer, buffer); } - if (currentBuffer.readableBytes() < QuarkusHttpPostBodyUtil.chunkSize) { + if (currentBuffer.readableBytes() < maxChunkSize) { currentData = null; return null; } @@ -1018,7 +1032,7 @@ private HttpContent encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEnco } // continue size -= buffer.readableBytes() + 1; - if (currentBuffer.readableBytes() >= QuarkusHttpPostBodyUtil.chunkSize) { + if (currentBuffer.readableBytes() >= maxChunkSize) { buffer = fillByteBuf(); return new DefaultHttpContent(buffer); } @@ -1052,7 +1066,7 @@ private HttpContent encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEnco currentBuffer = wrappedBuffer(currentBuffer, delimiter); } } - if (currentBuffer.readableBytes() >= QuarkusHttpPostBodyUtil.chunkSize) { + if (currentBuffer.readableBytes() >= maxChunkSize) { buffer = fillByteBuf(); return new DefaultHttpContent(buffer); } @@ -1075,7 +1089,7 @@ private HttpContent encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEnco } // end for current InterfaceHttpData, need more data - if (currentBuffer.readableBytes() < QuarkusHttpPostBodyUtil.chunkSize) { + if (currentBuffer.readableBytes() < maxChunkSize) { currentData = null; isKey = true; return null; @@ -1183,7 +1197,7 @@ private HttpContent nextChunk() throws ErrorDataEncoderException { } private int calculateRemainingSize() { - int size = QuarkusHttpPostBodyUtil.chunkSize; + int size = maxChunkSize; if (currentBuffer != null) { size -= currentBuffer.readableBytes(); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusHttpPostBodyUtil.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusHttpPostBodyUtil.java index f18dd5744559a..c259f3460419b 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusHttpPostBodyUtil.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusHttpPostBodyUtil.java @@ -28,8 +28,6 @@ */ final class QuarkusHttpPostBodyUtil { - public static final int chunkSize = 8096; - /** * Default Content-Type in binary form */ diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java index dabcbbbb57977..2210b3e6cf55f 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/multipart/QuarkusMultipartFormUpload.java @@ -42,6 +42,7 @@ public class QuarkusMultipartFormUpload implements ReadStream, Runnable public QuarkusMultipartFormUpload(Context context, QuarkusMultipartForm parts, boolean multipart, + int maxChunkSize, PausableHttpPostRequestEncoder.EncoderMode encoderMode) throws Exception { this.context = context; this.pending = new InboundBuffer<>(context) @@ -63,7 +64,8 @@ public FileUpload createFileUpload(HttpRequest request, String name, String file size); } }; - this.encoder = new PausableHttpPostRequestEncoder(httpDataFactory, request, multipart, charset, encoderMode); + this.encoder = new PausableHttpPostRequestEncoder(httpDataFactory, request, multipart, maxChunkSize, charset, + encoderMode); for (QuarkusMultipartFormDataPart formDataPart : parts) { if (formDataPart.isAttribute()) { encoder.addBodyAttribute(formDataPart.name(), formDataPart.value()); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/test/java/org/jboss/resteasy/reactive/client/impl/HandlerChainTest.java b/independent-projects/resteasy-reactive/client/runtime/src/test/java/org/jboss/resteasy/reactive/client/impl/HandlerChainTest.java index db1dda6733303..a22ee8643e8e7 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/test/java/org/jboss/resteasy/reactive/client/impl/HandlerChainTest.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/test/java/org/jboss/resteasy/reactive/client/impl/HandlerChainTest.java @@ -20,7 +20,7 @@ public class HandlerChainTest { @Test public void preSendHandlerIsAlwaysFirst() throws Exception { - var chain = new HandlerChain(true, LoggingScope.NONE, Collections.emptyMap(), new DefaultClientLogger()); + var chain = new HandlerChain(8096, true, LoggingScope.NONE, Collections.emptyMap(), new DefaultClientLogger()); ClientRestHandler preHandler = ctx -> { }; diff --git a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartChunksClient.java b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartChunksClient.java new file mode 100644 index 0000000000000..e70949e6f3b1c --- /dev/null +++ b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartChunksClient.java @@ -0,0 +1,16 @@ +package io.quarkus.it.rest.client.multipart; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.reactive.RestForm; + +@Path("/echo") +@RegisterRestClient(configKey = "multipart-chunks-client") +public interface MultipartChunksClient { + + @POST + @Path("/chunked") + String sendChunkedPayload(@RestForm byte[] data); +} diff --git a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java index d4b06b70f37eb..1207a618072ba 100644 --- a/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java +++ b/integration-tests/rest-client-reactive-multipart/src/main/java/io/quarkus/it/rest/client/multipart/MultipartResource.java @@ -9,6 +9,7 @@ import java.io.InputStream; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -21,7 +22,10 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Request; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.jboss.logging.Logger; @@ -56,6 +60,9 @@ public class MultipartResource { @RestClient MultipartClient client; + @RestClient + MultipartChunksClient chunkClient; + @GET @Path("/client/octet-stream") @Produces(MediaType.TEXT_PLAIN) @@ -373,6 +380,16 @@ public String sendPathAsTextParams() throws IOException { return client.sendPathAsTextFile(file, number); } + @GET + @Path("/client/chunked") + @Produces(MediaType.TEXT_PLAIN) + @Blocking + public String chunkRequest(@QueryParam("size") int size) { + byte[] data = new byte[size]; + Arrays.fill(data, (byte) 'A'); + return chunkClient.sendChunkedPayload(data); + } + @POST @Path("/echo/octet-stream") @Consumes(MediaType.APPLICATION_OCTET_STREAM) @@ -383,11 +400,19 @@ public String consumeOctetStream(File file) throws IOException { @POST @Path("/echo/binary") @Consumes(MediaType.MULTIPART_FORM_DATA) - public String consumeMultipart(@MultipartForm MultipartBodyWithBinaryFile body) { + public String consumeMultipart(@MultipartForm MultipartBodyWithBinaryFile body, Request request) { return String.format("fileOk:%s,nameOk:%s", body.file == null ? "null" : containsHelloWorld(body.file), GREETING_TXT.equals(body.fileName)); } + @POST + @Path("/echo/chunked") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String consumeChunkedRequest(Request request, @Context HttpHeaders headers) { + List values = headers.getRequestHeader("transfer-encoding"); + return "transfer-encodingOk:" + (!values.isEmpty() && values.get(0).equals("chunked")); + } + @POST @Path("/echo/text") @Consumes(MediaType.MULTIPART_FORM_DATA) diff --git a/integration-tests/rest-client-reactive-multipart/src/main/resources/application.properties b/integration-tests/rest-client-reactive-multipart/src/main/resources/application.properties index 351c0042ffc3f..723508885f11f 100644 --- a/integration-tests/rest-client-reactive-multipart/src/main/resources/application.properties +++ b/integration-tests/rest-client-reactive-multipart/src/main/resources/application.properties @@ -1 +1,4 @@ -multipart-client/mp-rest/url=${test.url} \ No newline at end of file +multipart-client/mp-rest/url=${test.url} +multipart-chunks-client/mp-rest/url=${test.url} + +quarkus.rest-client.multipart-chunks-client.multipart.max-chunk-size=1000 \ No newline at end of file diff --git a/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java b/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java index b13582f083276..bd13e6d21e6ce 100644 --- a/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java +++ b/integration-tests/rest-client-reactive-multipart/src/test/java/io/quarkus/it/rest/client/multipart/MultipartResourceTest.java @@ -333,6 +333,30 @@ public void shouldProducesInputStreamRestResponse() { .body(equalTo("HELLO WORLD")); } + @Test + public void shouldSendSingleChunk() { + RestAssured.given() + .queryParam("size", 1) + .when() + .get("/client/chunked") + .then() + .contentType(ContentType.TEXT) + .statusCode(200) + .body(equalTo("transfer-encodingOk:false")); + } + + @Test + public void shouldSendMultipleChunks() { + RestAssured.given() + .queryParam("size", 1000) + .when() + .get("/client/chunked") + .then() + .contentType(ContentType.TEXT) + .statusCode(200) + .body(equalTo("transfer-encodingOk:true")); + } + private void assertMultipartResponseContains(String response, String name, String contentType, Object value) { String[] lines = response.split("--"); assertThat(lines).anyMatch(line -> line.contains(String.format(EXPECTED_CONTENT_DISPOSITION_PART, name))