Skip to content

Commit 75117f4

Browse files
committed
Use the configured charset for part headers
This comment extends the use of the charset property in FormHttpMessageConverter to also include multipart headers with a default of UTF-8. We now also set the charset parameter of the "Content-Type" header to indicate to the server side how to decode correctly. Issue: SPR-15205
1 parent bda2723 commit 75117f4

File tree

2 files changed

+51
-18
lines changed

2 files changed

+51
-18
lines changed

spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java

+42-14
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.nio.charset.StandardCharsets;
2626
import java.util.ArrayList;
2727
import java.util.Collections;
28+
import java.util.HashMap;
2829
import java.util.Iterator;
2930
import java.util.List;
3031
import java.util.Map;
@@ -148,10 +149,17 @@ public void addPartConverter(HttpMessageConverter<?> partConverter) {
148149
/**
149150
* Set the default character set to use for reading and writing form data when
150151
* the request or response Content-Type header does not explicitly specify it.
151-
* <p>By default this is set to "UTF-8". As of 4.3, it will also be used as
152-
* the default charset for the conversion of text bodies in a multipart request.
153-
* In contrast to this, {@link #setMultipartCharset} only affects the encoding of
154-
* <i>file names</i> in a multipart request according to the encoded-word syntax.
152+
*
153+
* <p>As of 4.3, this is also used as the default charset for the conversion
154+
* of text bodies in a multipart request.
155+
*
156+
* <p>As of 5.0 this is also used for part headers including
157+
* "Content-Disposition" (and its filename parameter) unless (the mutually
158+
* exclusive) {@link #setMultipartCharset} is also set, in which case part
159+
* headers are encoded as ASCII and <i>filename</i> is encoded with the
160+
* "encoded-word" syntax from RFC 2047.
161+
*
162+
* <p>By default this is set to "UTF-8".
155163
*/
156164
public void setCharset(Charset charset) {
157165
if (charset != this.charset) {
@@ -177,9 +185,13 @@ private void applyDefaultCharset() {
177185

178186
/**
179187
* Set the character set to use when writing multipart data to encode file
180-
* names. Encoding is based on the encoded-word syntax defined in RFC 2047
188+
* names. Encoding is based on the "encoded-word" syntax defined in RFC 2047
181189
* and relies on {@code MimeUtility} from "javax.mail".
182-
* <p>If not set file names will be encoded as US-ASCII.
190+
*
191+
* <p>As of 5.0 by default part headers, including Content-Disposition (and
192+
* its filename parameter) will be encoded based on the setting of
193+
* {@link #setCharset(Charset)} or {@code UTF-8} by default.
194+
*
183195
* @since 4.1.1
184196
* @see <a href="http://en.wikipedia.org/wiki/MIME#Encoded-Word">Encoded-Word</a>
185197
*/
@@ -322,7 +334,11 @@ public void writeTo(OutputStream outputStream) throws IOException {
322334

323335
private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
324336
final byte[] boundary = generateMultipartBoundary();
325-
Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
337+
Map<String, String> parameters = new HashMap<>(2);
338+
parameters.put("boundary", new String(boundary, "US-ASCII"));
339+
if (!isFilenameCharsetSet()) {
340+
parameters.put("charset", this.charset.name());
341+
}
326342

327343
MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
328344
HttpHeaders headers = outputMessage.getHeaders();
@@ -344,6 +360,15 @@ public void writeTo(OutputStream outputStream) throws IOException {
344360
}
345361
}
346362

363+
/**
364+
* When {@link #setMultipartCharset(Charset)} is configured (i.e. RFC 2047,
365+
* "encoded-word" syntax) we need to use ASCII for part headers or otherwise
366+
* we encode directly using the configured {@link #setCharset(Charset)}.
367+
*/
368+
private boolean isFilenameCharsetSet() {
369+
return this.multipartCharset != null;
370+
}
371+
347372
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
348373
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
349374
String name = entry.getKey();
@@ -365,7 +390,8 @@ private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) t
365390
MediaType partContentType = partHeaders.getContentType();
366391
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
367392
if (messageConverter.canWrite(partType, partContentType)) {
368-
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
393+
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
394+
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
369395
multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
370396
if (!partHeaders.isEmpty()) {
371397
multipartMessage.getHeaders().putAll(partHeaders);
@@ -378,7 +404,6 @@ private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) t
378404
"found for request type [" + partType.getName() + "]");
379405
}
380406

381-
382407
/**
383408
* Generate a multipart boundary.
384409
* <p>This implementation delegates to
@@ -451,12 +476,15 @@ private static class MultipartHttpOutputMessage implements HttpOutputMessage {
451476

452477
private final OutputStream outputStream;
453478

479+
private final Charset charset;
480+
454481
private final HttpHeaders headers = new HttpHeaders();
455482

456483
private boolean headersWritten = false;
457484

458-
public MultipartHttpOutputMessage(OutputStream outputStream) {
485+
public MultipartHttpOutputMessage(OutputStream outputStream, Charset charset) {
459486
this.outputStream = outputStream;
487+
this.charset = charset;
460488
}
461489

462490
@Override
@@ -473,9 +501,9 @@ public OutputStream getBody() throws IOException {
473501
private void writeHeaders() throws IOException {
474502
if (!this.headersWritten) {
475503
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
476-
byte[] headerName = getAsciiBytes(entry.getKey());
504+
byte[] headerName = getBytes(entry.getKey());
477505
for (String headerValueString : entry.getValue()) {
478-
byte[] headerValue = getAsciiBytes(headerValueString);
506+
byte[] headerValue = getBytes(headerValueString);
479507
this.outputStream.write(headerName);
480508
this.outputStream.write(':');
481509
this.outputStream.write(' ');
@@ -488,8 +516,8 @@ private void writeHeaders() throws IOException {
488516
}
489517
}
490518

491-
private byte[] getAsciiBytes(String name) {
492-
return name.getBytes(StandardCharsets.US_ASCII);
519+
private byte[] getBytes(String name) {
520+
return name.getBytes(this.charset);
493521
}
494522
}
495523

spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java

+9-4
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,17 @@
4343
import org.springframework.util.LinkedMultiValueMap;
4444
import org.springframework.util.MultiValueMap;
4545

46-
import static org.hamcrest.CoreMatchers.*;
46+
import static org.hamcrest.CoreMatchers.allOf;
4747
import static org.hamcrest.CoreMatchers.endsWith;
4848
import static org.hamcrest.CoreMatchers.startsWith;
49-
import static org.junit.Assert.*;
50-
import static org.mockito.BDDMockito.*;
49+
import static org.junit.Assert.assertEquals;
50+
import static org.junit.Assert.assertFalse;
51+
import static org.junit.Assert.assertNotNull;
52+
import static org.junit.Assert.assertNull;
53+
import static org.junit.Assert.assertThat;
54+
import static org.junit.Assert.assertTrue;
55+
import static org.mockito.BDDMockito.never;
56+
import static org.mockito.BDDMockito.verify;
5157

5258
/**
5359
* @author Arjen Poutsma
@@ -138,7 +144,6 @@ public String getFilename() {
138144
parts.add("xml", entity);
139145

140146
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
141-
this.converter.setMultipartCharset(StandardCharsets.UTF_8);
142147
this.converter.write(parts, new MediaType("multipart", "form-data", StandardCharsets.UTF_8), outputMessage);
143148

144149
final MediaType contentType = outputMessage.getHeaders().getContentType();

0 commit comments

Comments
 (0)