Skip to content

Commit f5c1e2f

Browse files
committed
Polishing contribution
Includes small refactoring in DefaultServerWebExchange and adjustment of initMultipartData to get involved for any "multipart/" prefixed media type. In addition, "multipart/related" is now in the list of media types supported by FormHttpMessageConverter, which aligns it with MultipartHttpMessageReader. Closes gh-29671
1 parent 67df075 commit f5c1e2f

File tree

5 files changed

+75
-78
lines changed

5 files changed

+75
-78
lines changed

framework-docs/src/docs/asciidoc/web/webflux.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ The `DefaultServerWebExchange` uses the configured `HttpMessageReader` to parse
614614

615615
The `DefaultServerWebExchange` uses the configured
616616
`HttpMessageReader<MultiValueMap<String, Part>>` to parse `multipart/form-data`,
617-
`multipart/mixed` and `multipart/related` content into a `MultiValueMap`.
617+
`multipart/mixed`, and `multipart/related` content into a `MultiValueMap`.
618618
By default, this is the `DefaultPartHttpMessageReader`, which does not have any third-party
619619
dependencies.
620620
Alternatively, the `SynchronossPartHttpMessageReader` can be used, which is based on the
@@ -805,7 +805,7 @@ consistently for access to the cached form data versus reading from the raw requ
805805
==== Multipart
806806

807807
`MultipartHttpMessageReader` and `MultipartHttpMessageWriter` support decoding and
808-
encoding "multipart/form-data", "multipart/mixed" and "multipart/related" content.
808+
encoding "multipart/form-data", "multipart/mixed", and "multipart/related" content.
809809
In turn `MultipartHttpMessageReader` delegates to another `HttpMessageReader`
810810
for the actual parsing to a `Flux<Part>` and then simply collects the parts into a `MultiValueMap`.
811811
By default, the `DefaultPartHttpMessageReader` is used, but this can be changed through the

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -176,6 +176,7 @@ public FormHttpMessageConverter() {
176176
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
177177
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
178178
this.supportedMediaTypes.add(MediaType.MULTIPART_MIXED);
179+
this.supportedMediaTypes.add(MediaType.MULTIPART_RELATED);
179180

180181
this.partConverters.add(new ByteArrayHttpMessageConverter());
181182
this.partConverters.add(new StringHttpMessageConverter());

spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -135,50 +135,68 @@ public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse re
135135
this.applicationContext = applicationContext;
136136
}
137137

138-
@SuppressWarnings("unchecked")
139138
private static Mono<MultiValueMap<String, String>> initFormData(ServerHttpRequest request,
140139
ServerCodecConfigurer configurer, String logPrefix) {
141140

142-
try {
143-
MediaType contentType = request.getHeaders().getContentType();
144-
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
145-
return ((HttpMessageReader<MultiValueMap<String, String>>) configurer.getReaders().stream()
146-
.filter(reader -> reader.canRead(FORM_DATA_TYPE, MediaType.APPLICATION_FORM_URLENCODED))
147-
.findFirst()
148-
.orElseThrow(() -> new IllegalStateException("No form data HttpMessageReader.")))
149-
.readMono(FORM_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
150-
.switchIfEmpty(EMPTY_FORM_DATA)
151-
.cache();
152-
}
141+
MediaType contentType = getContentType(request);
142+
if (contentType == null || !contentType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) {
143+
return EMPTY_FORM_DATA;
153144
}
154-
catch (InvalidMediaTypeException ex) {
155-
// Ignore
145+
146+
HttpMessageReader<MultiValueMap<String, String>> reader = getReader(configurer, contentType, FORM_DATA_TYPE);
147+
if (reader == null) {
148+
return Mono.error(new IllegalStateException("No HttpMessageReader for " + contentType));
156149
}
157-
return EMPTY_FORM_DATA;
150+
151+
return reader
152+
.readMono(FORM_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
153+
.switchIfEmpty(EMPTY_FORM_DATA)
154+
.cache();
158155
}
159156

160-
@SuppressWarnings("unchecked")
161157
private static Mono<MultiValueMap<String, Part>> initMultipartData(ServerHttpRequest request,
162158
ServerCodecConfigurer configurer, String logPrefix) {
163159

160+
MediaType contentType = getContentType(request);
161+
if (contentType == null || !contentType.getType().equalsIgnoreCase("multipart")) {
162+
return EMPTY_MULTIPART_DATA;
163+
}
164+
165+
HttpMessageReader<MultiValueMap<String, Part>> reader = getReader(configurer, contentType, MULTIPART_DATA_TYPE);
166+
if (reader == null) {
167+
return Mono.error(new IllegalStateException("No HttpMessageReader for " + contentType));
168+
}
169+
170+
return reader
171+
.readMono(MULTIPART_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
172+
.switchIfEmpty(EMPTY_MULTIPART_DATA)
173+
.cache();
174+
}
175+
176+
@Nullable
177+
private static MediaType getContentType(ServerHttpRequest request) {
178+
MediaType contentType = null;
164179
try {
165-
MediaType contentType = request.getHeaders().getContentType();
166-
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType) ||
167-
MediaType.MULTIPART_MIXED.isCompatibleWith(contentType) ||
168-
MediaType.MULTIPART_RELATED.isCompatibleWith(contentType)) {
169-
return ((HttpMessageReader<MultiValueMap<String, Part>>) configurer.getReaders().stream()
170-
.filter(reader -> reader.canRead(MULTIPART_DATA_TYPE, contentType))
171-
.findFirst()
172-
.orElseThrow(() -> new IllegalStateException("No multipart HttpMessageReader.")))
173-
.readMono(MULTIPART_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
174-
.switchIfEmpty(EMPTY_MULTIPART_DATA)
175-
.cache();
176-
}
180+
contentType = request.getHeaders().getContentType();
177181
}
178182
catch (InvalidMediaTypeException ex) {
179-
// Ignore
183+
// ignore
184+
}
185+
return contentType;
186+
}
187+
188+
@SuppressWarnings("unchecked")
189+
@Nullable
190+
private static <E> HttpMessageReader<E> getReader(
191+
ServerCodecConfigurer configurer, MediaType contentType, ResolvableType targetType) {
192+
193+
HttpMessageReader<E> result = null;
194+
for (HttpMessageReader<?> reader : configurer.getReaders()) {
195+
if (reader.canRead(targetType, contentType)) {
196+
result = (HttpMessageReader<E>) reader;
197+
}
180198
}
181-
return EMPTY_MULTIPART_DATA;
199+
return result;
182200
}
183201

184202

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

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.io.InputStream;
2222
import java.io.StringReader;
2323
import java.nio.charset.StandardCharsets;
24-
import java.util.ArrayList;
2524
import java.util.LinkedHashMap;
2625
import java.util.List;
2726
import java.util.Map;
@@ -53,6 +52,7 @@
5352
import static org.springframework.http.MediaType.APPLICATION_JSON;
5453
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
5554
import static org.springframework.http.MediaType.MULTIPART_MIXED;
55+
import static org.springframework.http.MediaType.MULTIPART_RELATED;
5656
import static org.springframework.http.MediaType.TEXT_XML;
5757

5858
/**
@@ -66,8 +66,6 @@
6666
*/
6767
public class FormHttpMessageConverterTests {
6868

69-
private static final MediaType MULTIPART_RELATED = new MediaType("multipart", "related");
70-
7169
private final FormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter();
7270

7371

@@ -85,8 +83,6 @@ public void cannotReadMultipart() {
8583
// Without custom multipart types supported
8684
asssertCannotReadMultipart();
8785

88-
this.converter.addSupportedMediaTypes(MULTIPART_RELATED);
89-
9086
// Should still be the case with custom multipart types supported
9187
asssertCannotReadMultipart();
9288
}
@@ -96,28 +92,27 @@ public void canWrite() {
9692
assertCanWrite(APPLICATION_FORM_URLENCODED);
9793
assertCanWrite(MULTIPART_FORM_DATA);
9894
assertCanWrite(MULTIPART_MIXED);
95+
assertCanWrite(MULTIPART_RELATED);
9996
assertCanWrite(new MediaType("multipart", "form-data", StandardCharsets.UTF_8));
10097
assertCanWrite(MediaType.ALL);
10198
assertCanWrite(null);
10299
}
103100

104101
@Test
105102
public void setSupportedMediaTypes() {
106-
assertCannotWrite(MULTIPART_RELATED);
103+
this.converter.setSupportedMediaTypes(List.of(MULTIPART_FORM_DATA));
104+
assertCannotWrite(MULTIPART_MIXED);
107105

108-
List<MediaType> supportedMediaTypes = new ArrayList<>(this.converter.getSupportedMediaTypes());
109-
supportedMediaTypes.add(MULTIPART_RELATED);
110-
this.converter.setSupportedMediaTypes(supportedMediaTypes);
111-
112-
assertCanWrite(MULTIPART_RELATED);
106+
this.converter.setSupportedMediaTypes(List.of(MULTIPART_MIXED));
107+
assertCanWrite(MULTIPART_MIXED);
113108
}
114109

115110
@Test
116111
public void addSupportedMediaTypes() {
117-
assertCannotWrite(MULTIPART_RELATED);
112+
this.converter.setSupportedMediaTypes(List.of(MULTIPART_FORM_DATA));
113+
assertCannotWrite(MULTIPART_MIXED);
118114

119115
this.converter.addSupportedMediaTypes(MULTIPART_RELATED);
120-
121116
assertCanWrite(MULTIPART_RELATED);
122117
}
123118

spring-web/src/test/java/org/springframework/http/server/reactive/MultipartHttpHandlerIntegrationTests.java

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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,7 +32,6 @@
3232
import org.springframework.http.codec.multipart.FilePart;
3333
import org.springframework.http.codec.multipart.FormFieldPart;
3434
import org.springframework.http.codec.multipart.Part;
35-
import org.springframework.http.converter.FormHttpMessageConverter;
3635
import org.springframework.util.LinkedMultiValueMap;
3736
import org.springframework.util.MultiValueMap;
3837
import org.springframework.web.client.RestTemplate;
@@ -56,44 +55,23 @@ protected HttpHandler createHttpHandler() {
5655
}
5756

5857
@ParameterizedHttpServerTest
59-
void getFormPartsFormdata(HttpServer httpServer) throws Exception {
60-
performTest(httpServer, MediaType.MULTIPART_FORM_DATA);
58+
void getMultipartFormData(HttpServer httpServer) throws Exception {
59+
testMultipart(httpServer, MediaType.MULTIPART_FORM_DATA);
6160
}
6261

6362
@ParameterizedHttpServerTest
64-
void getFormPartsMixed(HttpServer httpServer) throws Exception {
65-
performTest(httpServer, MediaType.MULTIPART_MIXED);
63+
void getMultipartMixed(HttpServer httpServer) throws Exception {
64+
testMultipart(httpServer, MediaType.MULTIPART_MIXED);
6665
}
6766

6867
@ParameterizedHttpServerTest
69-
void getFormPartsRelated(HttpServer httpServer) throws Exception {
70-
RestTemplate restTemplate = new RestTemplate();
71-
restTemplate.getMessageConverters().stream()
72-
.filter(FormHttpMessageConverter.class::isInstance)
73-
.map(FormHttpMessageConverter.class::cast)
74-
.findFirst()
75-
.orElseThrow()
76-
.addSupportedMediaTypes(MediaType.MULTIPART_RELATED);
77-
performTest(httpServer, MediaType.MULTIPART_RELATED, restTemplate);
68+
void getMultipartRelated(HttpServer httpServer) throws Exception {
69+
testMultipart(httpServer, MediaType.MULTIPART_RELATED);
7870
}
7971

80-
private void performTest(HttpServer httpServer, MediaType mediaType) throws Exception {
81-
performTest(httpServer, mediaType, new RestTemplate());
82-
}
83-
84-
private void performTest(HttpServer httpServer, MediaType mediaType, RestTemplate restTemplate) throws Exception {
72+
private void testMultipart(HttpServer httpServer, MediaType mediaType) throws Exception {
8573
startServer(httpServer);
8674

87-
@SuppressWarnings("resource")
88-
RequestEntity<MultiValueMap<String, Object>> request = RequestEntity
89-
.post(URI.create("http://localhost:" + port + "/form-parts"))
90-
.contentType(mediaType)
91-
.body(generateBody());
92-
ResponseEntity<Void> response = restTemplate.exchange(request, Void.class);
93-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
94-
}
95-
96-
private MultiValueMap<String, Object> generateBody() {
9775
HttpHeaders fooHeaders = new HttpHeaders();
9876
fooHeaders.setContentType(MediaType.TEXT_PLAIN);
9977
ClassPathResource fooResource = new ClassPathResource("org/springframework/http/codec/multipart/foo.txt");
@@ -102,7 +80,12 @@ private MultiValueMap<String, Object> generateBody() {
10280
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
10381
parts.add("fooPart", fooPart);
10482
parts.add("barPart", barPart);
105-
return parts;
83+
84+
URI url = URI.create("http://localhost:" + port + "/form-parts");
85+
ResponseEntity<Void> response = new RestTemplate().exchange(
86+
RequestEntity.post(url).contentType(mediaType).body(parts), Void.class);
87+
88+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
10689
}
10790

10891

0 commit comments

Comments
 (0)