Skip to content

Commit 85be78d

Browse files
MultipartEntityBuilder to use a fixed boundary by a default (#619)
1 parent c687247 commit 85be78d

3 files changed

Lines changed: 98 additions & 22 deletions

File tree

httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,40 +29,53 @@
2929

3030
import java.io.File;
3131
import java.io.InputStream;
32-
import java.nio.CharBuffer;
3332
import java.nio.charset.Charset;
3433
import java.nio.charset.StandardCharsets;
3534
import java.util.ArrayList;
3635
import java.util.Collections;
3736
import java.util.List;
38-
import java.util.concurrent.ThreadLocalRandom;
37+
import java.util.UUID;
3938

4039
import org.apache.hc.core5.http.ContentType;
4140
import org.apache.hc.core5.http.HttpEntity;
4241
import org.apache.hc.core5.http.NameValuePair;
4342
import org.apache.hc.core5.http.message.BasicNameValuePair;
4443
import org.apache.hc.core5.util.Args;
44+
import org.slf4j.Logger;
45+
import org.slf4j.LoggerFactory;
4546

4647
/**
4748
* Builder for multipart {@link HttpEntity}s.
49+
* <p>
50+
* This class constructs multipart entities with a boundary determined by either a fixed
51+
* value ("httpclient_boundary_7k9p2m4x8n5j3q6t1r0vwyzabcdefghi") or a random UUID. If no
52+
* boundary is explicitly set via {@link #setBoundary(String)}, it defaults to the fixed
53+
* value unless {@link #withRandomBoundary()} is called to request a random UUID at build
54+
* time. Users can provide a custom boundary with {@link #setBoundary(String)}. A warning
55+
* is logged when no explicit boundary is set via {@link #setBoundary(String)}, encouraging
56+
* deliberate choice.
57+
* </p>
4858
*
4959
* @since 5.0
5060
*/
5161
public class MultipartEntityBuilder {
5262

53-
/**
54-
* The pool of ASCII chars to be used for generating a multipart boundary.
55-
*/
56-
private final static char[] MULTIPART_CHARS =
57-
"-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
58-
.toCharArray();
59-
6063
private ContentType contentType;
6164
private HttpMultipartMode mode = HttpMultipartMode.STRICT;
6265
private String boundary;
6366
private Charset charset;
6467
private List<MultipartPart> multipartParts;
6568

69+
70+
private static final String BOUNDARY_PREFIX = "httpclient_boundary_";
71+
72+
private boolean isRandomBoundaryRequested = false;
73+
/**
74+
* The logger for this class.
75+
*/
76+
private static final Logger LOG = LoggerFactory.getLogger(MultipartEntityBuilder.class);
77+
78+
6679
/**
6780
* The preamble of the multipart message.
6881
* This field stores the optional preamble that should be added at the beginning of the multipart message.
@@ -104,6 +117,17 @@ public MultipartEntityBuilder setStrictMode() {
104117
return this;
105118
}
106119

120+
/**
121+
* Sets a custom boundary string for the multipart entity.
122+
* <p>
123+
* If {@code null} is provided, the builder reverts to its default boundary logic:
124+
* either using a boundary from the {@code contentType} if present, or falling back
125+
* to a fixed or random boundary (depending on {@link #withRandomBoundary()}).
126+
* </p>
127+
*
128+
* @param boundary the boundary string, or {@code null} to use the default boundary logic
129+
* @return this builder instance
130+
*/
107131
public MultipartEntityBuilder setBoundary(final String boundary) {
108132
this.boundary = boundary;
109133
return this;
@@ -204,6 +228,20 @@ public MultipartEntityBuilder addBinaryBody(final String name, final InputStream
204228
return addBinaryBody(name, stream, ContentType.DEFAULT_BINARY, null);
205229
}
206230

231+
/**
232+
* Returns the fixed default boundary value.
233+
*/
234+
private String getFixedBoundary() {
235+
return BOUNDARY_PREFIX + "7k9p2m4x8n5j3q6t1r0vwyzabcdefghi";
236+
}
237+
238+
/**
239+
* Generates a random boundary using UUID.
240+
*/
241+
private String getRandomBoundary() {
242+
return BOUNDARY_PREFIX + UUID.randomUUID();
243+
}
244+
207245
/**
208246
* Adds a preamble to the multipart entity being constructed. The preamble is the text that appears before the first
209247
* boundary delimiter. The preamble is optional and may be null.
@@ -231,15 +269,17 @@ public MultipartEntityBuilder addEpilogue(final String epilogue) {
231269
return this;
232270
}
233271

234-
private String generateBoundary() {
235-
final ThreadLocalRandom rand = ThreadLocalRandom.current();
236-
final int count = rand.nextInt(30, 41); // a random size from 30 to 40
237-
final CharBuffer buffer = CharBuffer.allocate(count);
238-
while (buffer.hasRemaining()) {
239-
buffer.put(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
240-
}
241-
buffer.flip();
242-
return buffer.toString();
272+
/**
273+
* Configures the builder to request a random boundary generated by UUID.randomUUID()
274+
* at build time if no explicit boundary is set via {@link #setBoundary(String)}.
275+
*
276+
* @return this builder instance
277+
* @since 5.5
278+
*/
279+
public MultipartEntityBuilder withRandomBoundary() {
280+
this.isRandomBoundaryRequested = true;
281+
this.boundary = null;
282+
return this;
243283
}
244284

245285
MultipartFormEntity buildEntity() {
@@ -248,7 +288,10 @@ MultipartFormEntity buildEntity() {
248288
boundaryCopy = contentType.getParameter("boundary");
249289
}
250290
if (boundaryCopy == null) {
251-
boundaryCopy = generateBoundary();
291+
boundaryCopy = isRandomBoundaryRequested ? getRandomBoundary() : getFixedBoundary();
292+
if (LOG.isWarnEnabled()) {
293+
LOG.warn("No boundary explicitly set; using generated default: {}", boundaryCopy);
294+
}
252295
}
253296
Charset charsetCopy = charset;
254297
if (charsetCopy == null && contentType != null) {

httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@
3535
import java.util.List;
3636

3737
import org.apache.hc.core5.http.ContentType;
38+
import org.apache.hc.core5.http.HeaderElement;
3839
import org.apache.hc.core5.http.NameValuePair;
40+
import org.apache.hc.core5.http.message.BasicHeaderValueParser;
3941
import org.apache.hc.core5.http.message.BasicNameValuePair;
42+
import org.apache.hc.core5.http.message.ParserCursor;
4043
import org.junit.jupiter.api.Assertions;
4144
import org.junit.jupiter.api.Test;
4245

@@ -307,4 +310,36 @@ void testMultipartWriteToRFC7578ModeWithFilenameStar() throws Exception {
307310
"--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.ISO_8859_1.name()));
308311
}
309312

313+
@Test
314+
void testRandomBoundary() {
315+
final MultipartFormEntity entity = MultipartEntityBuilder.create()
316+
.withRandomBoundary()
317+
.buildEntity();
318+
final NameValuePair boundaryParam = extractBoundary(entity.getContentType());
319+
final String boundary = boundaryParam.getValue();
320+
Assertions.assertNotNull(boundary);
321+
Assertions.assertEquals(56, boundary.length());
322+
Assertions.assertTrue(boundary.startsWith("httpclient_boundary_"));
323+
Assertions.assertTrue(boundary.substring(20).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"));
324+
}
325+
326+
@Test
327+
void testExplicitBoundaryOverridesRandom() {
328+
final String customBoundary = "my_custom_boundary";
329+
final MultipartFormEntity entity = MultipartEntityBuilder.create()
330+
.withRandomBoundary()
331+
.setBoundary(customBoundary)
332+
.buildEntity();
333+
final NameValuePair boundaryParam = extractBoundary(entity.getContentType());
334+
Assertions.assertEquals(customBoundary, boundaryParam.getValue());
335+
}
336+
337+
private NameValuePair extractBoundary(final String contentType) {
338+
final BasicHeaderValueParser parser = BasicHeaderValueParser.INSTANCE;
339+
final ParserCursor cursor = new ParserCursor(0, contentType.length());
340+
final HeaderElement elem = parser.parseHeaderElement(contentType, cursor);
341+
Assertions.assertEquals("multipart/mixed", elem.getName());
342+
return elem.getParameterByName("boundary");
343+
}
344+
310345
}

httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartFormHttpEntity.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ void testImplicitContractorParams() {
7878
final String boundary = p1.getValue();
7979
Assertions.assertNotNull(boundary);
8080

81-
Assertions.assertTrue(boundary.length() >= 30);
82-
Assertions.assertTrue(boundary.length() <= 40);
83-
81+
Assertions.assertEquals(52, boundary.length());
8482
final NameValuePair p2 = elem.getParameterByName("charset");
8583
Assertions.assertNull(p2);
8684
}

0 commit comments

Comments
 (0)