Skip to content

Commit b94aa40

Browse files
Merge branch 'main' of github.com:flowable/flowable-engine
2 parents bd8e3e7 + 1c5b8bc commit b94aa40

6 files changed

Lines changed: 267 additions & 4 deletions

File tree

modules/flowable-http-common/src/main/java/org/flowable/http/common/api/MultiValuePart.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public static MultiValuePart fromText(String name, String value) {
5656
return new MultiValuePart(name, value, null);
5757
}
5858

59+
public static MultiValuePart fromText(String name, String value, String mimeType) {
60+
return new MultiValuePart(name, value, null, mimeType);
61+
}
62+
5963
public static MultiValuePart fromFile(String name, byte[] value, String filename) {
6064
return new MultiValuePart(name, value, filename);
6165
}

modules/flowable-http-common/src/main/java/org/flowable/http/common/impl/HttpClientConfig.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,22 @@ public class HttpClientConfig {
9292

9393
protected boolean useSystemProperties = false;
9494

95+
/**
96+
* The multipart mode to use when building multipart/form-data requests with the Apache HTTP client implementations.
97+
* <p>
98+
* Supported values:
99+
* <ul>
100+
* <li>{@code "STRICT"} - RFC 2046 compliant. Always writes Content-Type headers for all parts,
101+
* including text parts. This is the default and is needed for text parts with a custom mime type to work correctly.</li>
102+
* <li>{@code "BROWSER_COMPATIBLE"} - Mimics browser behavior. Only writes Content-Type headers for parts with a filename.
103+
* This was the default before 8.0. Note that text parts with a custom mime type will not have their Content-Type sent
104+
* in this mode.</li>
105+
* </ul>
106+
* <p>
107+
* This setting has no effect on the Spring WebClient implementation.
108+
*/
109+
protected String multipartMode = "STRICT";
110+
95111
protected FlowableHttpClient httpClient;
96112
protected Runnable closeRunnable;
97113

@@ -148,6 +164,14 @@ public boolean isUseSystemProperties() {
148164
return useSystemProperties;
149165
}
150166

167+
public String getMultipartMode() {
168+
return multipartMode;
169+
}
170+
171+
public void setMultipartMode(String multipartMode) {
172+
this.multipartMode = multipartMode;
173+
}
174+
151175
public void merge(HttpClientConfig other) {
152176
if (this.connectTimeout != other.getConnectTimeout()) {
153177
setConnectTimeout(other.getConnectTimeout());
@@ -173,6 +197,10 @@ public void merge(HttpClientConfig other) {
173197
setUseSystemProperties(other.isUseSystemProperties());
174198
}
175199

200+
if (!Objects.equals(this.multipartMode, other.getMultipartMode())) {
201+
setMultipartMode(other.getMultipartMode());
202+
}
203+
176204
if (!Objects.equals(this.httpClient, other.getHttpClient())) {
177205
setHttpClient(other.getHttpClient());
178206
}

modules/flowable-http-common/src/main/java/org/flowable/http/common/impl/apache/ApacheHttpComponentsFlowableHttpClient.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public class ApacheHttpComponentsFlowableHttpClient implements FlowableHttpClien
102102
protected final Logger logger = LoggerFactory.getLogger(getClass());
103103

104104
protected HttpClientBuilder clientBuilder;
105+
protected HttpMultipartMode multipartMode;
105106
protected int socketTimeout;
106107
protected int connectTimeout;
107108
protected int connectionRequestTimeout;
@@ -149,6 +150,7 @@ public boolean verify(String s, SSLSession sslSession) {
149150

150151
this.clientBuilder = httpClientBuilder;
151152

153+
this.multipartMode = resolveMultipartMode(config.getMultipartMode());
152154
this.socketTimeout = config.getSocketTimeout();
153155
this.connectTimeout = config.getConnectTimeout();
154156
this.connectionRequestTimeout = config.getConnectionRequestTimeout();
@@ -157,11 +159,24 @@ public boolean verify(String s, SSLSession sslSession) {
157159
public ApacheHttpComponentsFlowableHttpClient(HttpClientBuilder clientBuilder, int socketTimeout, int connectTimeout,
158160
int connectionRequestTimeout) {
159161
this.clientBuilder = clientBuilder;
162+
this.multipartMode = HttpMultipartMode.STRICT;
160163
this.socketTimeout = socketTimeout;
161164
this.connectTimeout = connectTimeout;
162165
this.connectionRequestTimeout = connectionRequestTimeout;
163166
}
164167

168+
protected static HttpMultipartMode resolveMultipartMode(String mode) {
169+
if (mode == null) {
170+
return HttpMultipartMode.STRICT;
171+
}
172+
return switch (mode.toUpperCase()) {
173+
case "BROWSER_COMPATIBLE" -> HttpMultipartMode.BROWSER_COMPATIBLE;
174+
case "STRICT" -> HttpMultipartMode.STRICT;
175+
default -> throw new FlowableIllegalArgumentException("Unsupported multipart mode: " + mode
176+
+ ". Supported values are: STRICT, BROWSER_COMPATIBLE");
177+
};
178+
}
179+
165180
@Override
166181
public ExecutableHttpRequest prepareRequest(HttpRequest requestInfo) {
167182
try {
@@ -235,7 +250,7 @@ protected void setRequestEntity(HttpRequest requestInfo, HttpEntityEnclosingRequ
235250
} else if (requestInfo.getMultiValueParts() != null) {
236251
if (MULTIPART_ENTITY_BUILDER_PRESENT) {
237252
MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
238-
entityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
253+
entityBuilder.setMode(multipartMode);
239254
for (MultiValuePart part : requestInfo.getMultiValueParts()) {
240255
String name = part.getName();
241256
Object value = part.getBody();
@@ -246,7 +261,11 @@ protected void setRequestEntity(HttpRequest requestInfo, HttpEntityEnclosingRequ
246261
entityBuilder.addBinaryBody(name, (byte[]) value, ContentType.DEFAULT_BINARY, part.getFilename());
247262
}
248263
} else if (value instanceof String) {
249-
entityBuilder.addTextBody(name, (String) value);
264+
if (StringUtils.isNotBlank(part.getMimeType())) {
265+
entityBuilder.addTextBody(name, (String) value, ContentType.create(part.getMimeType()));
266+
} else {
267+
entityBuilder.addTextBody(name, (String) value);
268+
}
250269
} else if (value != null) {
251270
throw new FlowableIllegalArgumentException("Value of type " + value.getClass() + " is not supported as multi part content");
252271
}

modules/flowable-http-common/src/main/java/org/flowable/http/common/impl/apache/client5/ApacheHttpComponents5FlowableHttpClient.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public class ApacheHttpComponents5FlowableHttpClient implements FlowableAsyncHtt
8686

8787
protected HttpAsyncClient client;
8888
protected boolean closeClient;
89+
protected HttpMultipartMode multipartMode;
8990
protected int socketTimeout;
9091
protected int connectTimeout;
9192
protected int connectionRequestTimeout;
@@ -133,6 +134,7 @@ public ApacheHttpComponents5FlowableHttpClient(HttpClientConfig config, Consumer
133134
this.client = client;
134135
this.closeClient = true;
135136

137+
this.multipartMode = resolveMultipartMode(config.getMultipartMode());
136138
this.socketTimeout = config.getSocketTimeout();
137139
this.connectTimeout = config.getConnectTimeout();
138140
this.connectionRequestTimeout = config.getConnectionRequestTimeout();
@@ -141,11 +143,25 @@ public ApacheHttpComponents5FlowableHttpClient(HttpClientConfig config, Consumer
141143
public ApacheHttpComponents5FlowableHttpClient(HttpAsyncClient client, int socketTimeout, int connectTimeout,
142144
int connectionRequestTimeout) {
143145
this.client = client;
146+
this.multipartMode = HttpMultipartMode.STRICT;
144147
this.socketTimeout = socketTimeout;
145148
this.connectTimeout = connectTimeout;
146149
this.connectionRequestTimeout = connectionRequestTimeout;
147150
}
148151

152+
protected static HttpMultipartMode resolveMultipartMode(String mode) {
153+
if (mode == null) {
154+
return HttpMultipartMode.STRICT;
155+
}
156+
return switch (mode.toUpperCase()) {
157+
case "BROWSER_COMPATIBLE", "LEGACY" -> HttpMultipartMode.LEGACY;
158+
case "STRICT" -> HttpMultipartMode.STRICT;
159+
case "EXTENDED" -> HttpMultipartMode.EXTENDED;
160+
default -> throw new FlowableIllegalArgumentException("Unsupported multipart mode: " + mode
161+
+ ". Supported values are: STRICT, BROWSER_COMPATIBLE, LEGACY, EXTENDED");
162+
};
163+
}
164+
149165
public void close() {
150166
if (closeClient && client instanceof ModalCloseable) {
151167
((ModalCloseable) client).close(CloseMode.GRACEFUL);
@@ -220,7 +236,7 @@ protected void setRequestEntity(HttpRequest requestInfo, AsyncRequestBuilder req
220236
}
221237
} else if (requestInfo.getMultiValueParts() != null) {
222238
MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
223-
entityBuilder.setMode(HttpMultipartMode.LEGACY);
239+
entityBuilder.setMode(multipartMode);
224240
for (MultiValuePart part : requestInfo.getMultiValueParts()) {
225241
String name = part.getName();
226242
Object value = part.getBody();
@@ -231,7 +247,11 @@ protected void setRequestEntity(HttpRequest requestInfo, AsyncRequestBuilder req
231247
entityBuilder.addBinaryBody(name, (byte[]) value, ContentType.DEFAULT_BINARY, part.getFilename());
232248
}
233249
} else if (value instanceof String) {
234-
entityBuilder.addTextBody(name, (String) value);
250+
if (StringUtils.isNotBlank(part.getMimeType())) {
251+
entityBuilder.addTextBody(name, (String) value, ContentType.create(part.getMimeType()));
252+
} else {
253+
entityBuilder.addTextBody(name, (String) value);
254+
}
235255
} else if (value != null) {
236256
throw new FlowableIllegalArgumentException("Value of type " + value.getClass() + " is not supported as multi part content");
237257
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* Licensed under the Apache License, Version 2.0 (the "License");
2+
* you may not use this file except in compliance with the License.
3+
* You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
package org.flowable.http;
14+
15+
import java.time.Duration;
16+
import java.util.stream.Stream;
17+
18+
import org.flowable.http.common.impl.HttpClientConfig;
19+
import org.flowable.http.common.impl.apache.ApacheHttpComponentsFlowableHttpClient;
20+
import org.flowable.http.common.impl.apache.client5.ApacheHttpComponents5FlowableHttpClient;
21+
import org.junit.jupiter.api.extension.ExtensionContext;
22+
import org.junit.jupiter.params.provider.Arguments;
23+
import org.junit.jupiter.params.provider.ArgumentsProvider;
24+
import org.junit.jupiter.params.support.ParameterDeclarations;
25+
26+
/**
27+
* @author Filip Hrisafov
28+
*/
29+
public class BrowserCompatibleApacheHttpClientArgumentProvider implements ArgumentsProvider {
30+
31+
@Override
32+
public Stream<? extends Arguments> provideArguments(ParameterDeclarations parameters, ExtensionContext context) {
33+
HttpClientConfig config = createClientConfig();
34+
return Stream.of(
35+
Arguments.of(new ApacheHttpComponentsFlowableHttpClient(config)),
36+
Arguments.of(new ApacheHttpComponents5FlowableHttpClient(config))
37+
);
38+
}
39+
40+
protected HttpClientConfig createClientConfig() {
41+
HttpClientConfig config = new HttpClientConfig();
42+
config.setConnectTimeout(Duration.ofSeconds(5));
43+
config.setSocketTimeout(Duration.ofSeconds(5));
44+
config.setConnectionRequestTimeout(Duration.ofSeconds(5));
45+
config.setRequestRetryLimit(5);
46+
config.setDisableCertVerify(true);
47+
config.setMultipartMode("BROWSER_COMPATIBLE");
48+
49+
return config;
50+
}
51+
52+
}

0 commit comments

Comments
 (0)