Skip to content

Commit 646fcc5

Browse files
committed
Support Custom Headers for Multipart Async Data
This commit makes sure there is no custom Content-Disposition header before setting one automatically. This commit also adds a headers(Consumer<HttpHeaders>) method, so that one can user the nicer methods of HttpHeaders, as opposed to basic strings. Issue: SPR-16376
1 parent b2ce98e commit 646fcc5

File tree

3 files changed

+71
-16
lines changed

3 files changed

+71
-16
lines changed

spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.Arrays;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.function.Consumer;
2223

2324
import org.reactivestreams.Publisher;
2425

@@ -187,6 +188,13 @@ public interface PartBuilder {
187188
* @see HttpHeaders#add(String, String)
188189
*/
189190
PartBuilder header(String headerName, String... headerValues);
191+
192+
/**
193+
* Manipulate the part's headers with the given consumer.
194+
* @param headersConsumer a function that consumes the {@code HttpHeaders}
195+
* @return this builder
196+
*/
197+
PartBuilder headers(Consumer<HttpHeaders> headersConsumer);
190198
}
191199

192200

@@ -208,6 +216,13 @@ public PartBuilder header(String headerName, String... headerValues) {
208216
return this;
209217
}
210218

219+
@Override
220+
public PartBuilder headers(Consumer<HttpHeaders> headersConsumer) {
221+
Assert.notNull(headersConsumer, "'headersConsumer' must not be null");
222+
headersConsumer.accept(this.headers);
223+
return this;
224+
}
225+
211226
public HttpEntity<?> build() {
212227
return new HttpEntity<>(this.body, this.headers);
213228
}

spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java

+15-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -229,12 +229,13 @@ private Flux<DataBuffer> encodePartValues(byte[] boundary, String name, List<?>
229229
@SuppressWarnings("unchecked")
230230
private <T> Flux<DataBuffer> encodePart(byte[] boundary, String name, T value) {
231231
MultipartHttpOutputMessage outputMessage = new MultipartHttpOutputMessage(this.bufferFactory, getCharset());
232+
HttpHeaders outputHeaders = outputMessage.getHeaders();
232233

233234
T body;
234235
ResolvableType resolvableType = null;
235236
if (value instanceof HttpEntity) {
236237
HttpEntity<T> httpEntity = (HttpEntity<T>) value;
237-
outputMessage.getHeaders().putAll(httpEntity.getHeaders());
238+
outputHeaders.putAll(httpEntity.getHeaders());
238239
body = httpEntity.getBody();
239240
Assert.state(body != null, "MultipartHttpMessageWriter only supports HttpEntity with body");
240241

@@ -247,24 +248,24 @@ private <T> Flux<DataBuffer> encodePart(byte[] boundary, String name, T value) {
247248
else {
248249
body = value;
249250
}
250-
251251
if (resolvableType == null) {
252252
resolvableType = ResolvableType.forClass(body.getClass());
253253
}
254254

255-
if (body instanceof Resource) {
256-
outputMessage.getHeaders().setContentDispositionFormData(name, ((Resource) body).getFilename());
257-
}
258-
else if (Resource.class.equals(resolvableType.getRawClass())) {
259-
body = (T) Mono.from((Publisher<?>) body).doOnNext(o -> {
260-
outputMessage.getHeaders().setContentDispositionFormData(name, ((Resource) o).getFilename());
261-
});
262-
}
263-
else {
264-
outputMessage.getHeaders().setContentDispositionFormData(name, null);
255+
if (!outputHeaders.containsKey(HttpHeaders.CONTENT_DISPOSITION)) {
256+
if (body instanceof Resource) {
257+
outputHeaders.setContentDispositionFormData(name, ((Resource) body).getFilename());
258+
}
259+
else if (Resource.class.equals(resolvableType.getRawClass())) {
260+
body = (T) Mono.from((Publisher<?>) body).doOnNext(o -> outputHeaders
261+
.setContentDispositionFormData(name, ((Resource) o).getFilename()));
262+
}
263+
else {
264+
outputHeaders.setContentDispositionFormData(name, null);
265+
}
265266
}
266267

267-
MediaType contentType = outputMessage.getHeaders().getContentType();
268+
MediaType contentType = outputHeaders.getContentType();
268269

269270
final ResolvableType finalBodyType = resolvableType;
270271
Optional<HttpMessageWriter<?>> writer = this.partWriters.stream()

spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,6 +32,9 @@
3232
import org.springframework.core.codec.StringDecoder;
3333
import org.springframework.core.io.ClassPathResource;
3434
import org.springframework.core.io.Resource;
35+
import org.springframework.core.io.buffer.DataBuffer;
36+
import org.springframework.core.io.buffer.DataBufferUtils;
37+
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
3538
import org.springframework.http.HttpEntity;
3639
import org.springframework.http.MediaType;
3740
import org.springframework.http.client.MultipartBodyBuilder;
@@ -191,6 +194,42 @@ public void singleSubscriberWithStrings() {
191194
this.writer.write(result, null, MediaType.MULTIPART_FORM_DATA, response, hints).block();
192195
}
193196

197+
@Test // SPR-16376
198+
public void customContentDisposition() throws IOException {
199+
Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
200+
Flux<DataBuffer> buffers = DataBufferUtils.read(logo, new DefaultDataBufferFactory(), 1024);
201+
long contentLength = logo.contentLength();
202+
203+
MultipartBodyBuilder bodyBuilder = new MultipartBodyBuilder();
204+
bodyBuilder.part("resource", logo)
205+
.headers(h -> h.setContentDispositionFormData("resource", "spring.jpg"));
206+
bodyBuilder.asyncPart("buffers", buffers, DataBuffer.class)
207+
.headers(h -> {
208+
h.setContentDispositionFormData("buffers", "buffers.jpg");
209+
h.setContentType(MediaType.IMAGE_JPEG);
210+
h.setContentLength(contentLength);
211+
});
212+
213+
MultiValueMap<String, HttpEntity<?>> multipartData = bodyBuilder.build();
214+
215+
MockServerHttpResponse response = new MockServerHttpResponse();
216+
Map<String, Object> hints = Collections.emptyMap();
217+
this.writer.write(Mono.just(multipartData), null, MediaType.MULTIPART_FORM_DATA, response, hints).block();
218+
219+
MultiValueMap<String, Part> requestParts = parse(response, hints);
220+
assertEquals(2, requestParts.size());
221+
222+
Part part = requestParts.getFirst("resource");
223+
assertTrue(part instanceof FilePart);
224+
assertEquals("spring.jpg", ((FilePart) part).filename());
225+
assertEquals(logo.getFile().length(), part.headers().getContentLength());
226+
227+
part = requestParts.getFirst("buffers");
228+
assertTrue(part instanceof FilePart);
229+
assertEquals("buffers.jpg", ((FilePart) part).filename());
230+
assertEquals(logo.getFile().length(), part.headers().getContentLength());
231+
}
232+
194233
private MultiValueMap<String, Part> parse(MockServerHttpResponse response, Map<String, Object> hints) {
195234
MediaType contentType = response.getHeaders().getContentType();
196235
assertNotNull("No boundary found", contentType.getParameter("boundary"));

0 commit comments

Comments
 (0)