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
This commit is contained in:
rstoyanchev 2023-02-13 11:35:47 +00:00
parent 67df0756cd
commit f5c1e2ffa1
5 changed files with 75 additions and 78 deletions

View File

@ -614,7 +614,7 @@ The `DefaultServerWebExchange` uses the configured `HttpMessageReader` to parse
The `DefaultServerWebExchange` uses the configured
`HttpMessageReader<MultiValueMap<String, Part>>` to parse `multipart/form-data`,
`multipart/mixed` and `multipart/related` content into a `MultiValueMap`.
`multipart/mixed`, and `multipart/related` content into a `MultiValueMap`.
By default, this is the `DefaultPartHttpMessageReader`, which does not have any third-party
dependencies.
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
==== Multipart
`MultipartHttpMessageReader` and `MultipartHttpMessageWriter` support decoding and
encoding "multipart/form-data", "multipart/mixed" and "multipart/related" content.
encoding "multipart/form-data", "multipart/mixed", and "multipart/related" content.
In turn `MultipartHttpMessageReader` delegates to another `HttpMessageReader`
for the actual parsing to a `Flux<Part>` and then simply collects the parts into a `MultiValueMap`.
By default, the `DefaultPartHttpMessageReader` is used, but this can be changed through the

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -176,6 +176,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
this.supportedMediaTypes.add(MediaType.MULTIPART_MIXED);
this.supportedMediaTypes.add(MediaType.MULTIPART_RELATED);
this.partConverters.add(new ByteArrayHttpMessageConverter());
this.partConverters.add(new StringHttpMessageConverter());

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -135,50 +135,68 @@ public class DefaultServerWebExchange implements ServerWebExchange {
this.applicationContext = applicationContext;
}
@SuppressWarnings("unchecked")
private static Mono<MultiValueMap<String, String>> initFormData(ServerHttpRequest request,
ServerCodecConfigurer configurer, String logPrefix) {
try {
MediaType contentType = request.getHeaders().getContentType();
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
return ((HttpMessageReader<MultiValueMap<String, String>>) configurer.getReaders().stream()
.filter(reader -> reader.canRead(FORM_DATA_TYPE, MediaType.APPLICATION_FORM_URLENCODED))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No form data HttpMessageReader.")))
.readMono(FORM_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
.switchIfEmpty(EMPTY_FORM_DATA)
.cache();
}
MediaType contentType = getContentType(request);
if (contentType == null || !contentType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) {
return EMPTY_FORM_DATA;
}
catch (InvalidMediaTypeException ex) {
// Ignore
HttpMessageReader<MultiValueMap<String, String>> reader = getReader(configurer, contentType, FORM_DATA_TYPE);
if (reader == null) {
return Mono.error(new IllegalStateException("No HttpMessageReader for " + contentType));
}
return EMPTY_FORM_DATA;
return reader
.readMono(FORM_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
.switchIfEmpty(EMPTY_FORM_DATA)
.cache();
}
@SuppressWarnings("unchecked")
private static Mono<MultiValueMap<String, Part>> initMultipartData(ServerHttpRequest request,
ServerCodecConfigurer configurer, String logPrefix) {
MediaType contentType = getContentType(request);
if (contentType == null || !contentType.getType().equalsIgnoreCase("multipart")) {
return EMPTY_MULTIPART_DATA;
}
HttpMessageReader<MultiValueMap<String, Part>> reader = getReader(configurer, contentType, MULTIPART_DATA_TYPE);
if (reader == null) {
return Mono.error(new IllegalStateException("No HttpMessageReader for " + contentType));
}
return reader
.readMono(MULTIPART_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
.switchIfEmpty(EMPTY_MULTIPART_DATA)
.cache();
}
@Nullable
private static MediaType getContentType(ServerHttpRequest request) {
MediaType contentType = null;
try {
MediaType contentType = request.getHeaders().getContentType();
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType) ||
MediaType.MULTIPART_MIXED.isCompatibleWith(contentType) ||
MediaType.MULTIPART_RELATED.isCompatibleWith(contentType)) {
return ((HttpMessageReader<MultiValueMap<String, Part>>) configurer.getReaders().stream()
.filter(reader -> reader.canRead(MULTIPART_DATA_TYPE, contentType))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No multipart HttpMessageReader.")))
.readMono(MULTIPART_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
.switchIfEmpty(EMPTY_MULTIPART_DATA)
.cache();
}
contentType = request.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
// Ignore
// ignore
}
return EMPTY_MULTIPART_DATA;
return contentType;
}
@SuppressWarnings("unchecked")
@Nullable
private static <E> HttpMessageReader<E> getReader(
ServerCodecConfigurer configurer, MediaType contentType, ResolvableType targetType) {
HttpMessageReader<E> result = null;
for (HttpMessageReader<?> reader : configurer.getReaders()) {
if (reader.canRead(targetType, contentType)) {
result = (HttpMessageReader<E>) reader;
}
}
return result;
}

View File

@ -21,7 +21,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -53,6 +52,7 @@ import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
import static org.springframework.http.MediaType.MULTIPART_MIXED;
import static org.springframework.http.MediaType.MULTIPART_RELATED;
import static org.springframework.http.MediaType.TEXT_XML;
/**
@ -66,8 +66,6 @@ import static org.springframework.http.MediaType.TEXT_XML;
*/
public class FormHttpMessageConverterTests {
private static final MediaType MULTIPART_RELATED = new MediaType("multipart", "related");
private final FormHttpMessageConverter converter = new AllEncompassingFormHttpMessageConverter();
@ -85,8 +83,6 @@ public class FormHttpMessageConverterTests {
// Without custom multipart types supported
asssertCannotReadMultipart();
this.converter.addSupportedMediaTypes(MULTIPART_RELATED);
// Should still be the case with custom multipart types supported
asssertCannotReadMultipart();
}
@ -96,6 +92,7 @@ public class FormHttpMessageConverterTests {
assertCanWrite(APPLICATION_FORM_URLENCODED);
assertCanWrite(MULTIPART_FORM_DATA);
assertCanWrite(MULTIPART_MIXED);
assertCanWrite(MULTIPART_RELATED);
assertCanWrite(new MediaType("multipart", "form-data", StandardCharsets.UTF_8));
assertCanWrite(MediaType.ALL);
assertCanWrite(null);
@ -103,21 +100,19 @@ public class FormHttpMessageConverterTests {
@Test
public void setSupportedMediaTypes() {
assertCannotWrite(MULTIPART_RELATED);
this.converter.setSupportedMediaTypes(List.of(MULTIPART_FORM_DATA));
assertCannotWrite(MULTIPART_MIXED);
List<MediaType> supportedMediaTypes = new ArrayList<>(this.converter.getSupportedMediaTypes());
supportedMediaTypes.add(MULTIPART_RELATED);
this.converter.setSupportedMediaTypes(supportedMediaTypes);
assertCanWrite(MULTIPART_RELATED);
this.converter.setSupportedMediaTypes(List.of(MULTIPART_MIXED));
assertCanWrite(MULTIPART_MIXED);
}
@Test
public void addSupportedMediaTypes() {
assertCannotWrite(MULTIPART_RELATED);
this.converter.setSupportedMediaTypes(List.of(MULTIPART_FORM_DATA));
assertCannotWrite(MULTIPART_MIXED);
this.converter.addSupportedMediaTypes(MULTIPART_RELATED);
assertCanWrite(MULTIPART_RELATED);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,7 +32,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.codec.multipart.FormFieldPart;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
@ -56,44 +55,23 @@ class MultipartHttpHandlerIntegrationTests extends AbstractHttpHandlerIntegratio
}
@ParameterizedHttpServerTest
void getFormPartsFormdata(HttpServer httpServer) throws Exception {
performTest(httpServer, MediaType.MULTIPART_FORM_DATA);
void getMultipartFormData(HttpServer httpServer) throws Exception {
testMultipart(httpServer, MediaType.MULTIPART_FORM_DATA);
}
@ParameterizedHttpServerTest
void getFormPartsMixed(HttpServer httpServer) throws Exception {
performTest(httpServer, MediaType.MULTIPART_MIXED);
void getMultipartMixed(HttpServer httpServer) throws Exception {
testMultipart(httpServer, MediaType.MULTIPART_MIXED);
}
@ParameterizedHttpServerTest
void getFormPartsRelated(HttpServer httpServer) throws Exception {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().stream()
.filter(FormHttpMessageConverter.class::isInstance)
.map(FormHttpMessageConverter.class::cast)
.findFirst()
.orElseThrow()
.addSupportedMediaTypes(MediaType.MULTIPART_RELATED);
performTest(httpServer, MediaType.MULTIPART_RELATED, restTemplate);
void getMultipartRelated(HttpServer httpServer) throws Exception {
testMultipart(httpServer, MediaType.MULTIPART_RELATED);
}
private void performTest(HttpServer httpServer, MediaType mediaType) throws Exception {
performTest(httpServer, mediaType, new RestTemplate());
}
private void performTest(HttpServer httpServer, MediaType mediaType, RestTemplate restTemplate) throws Exception {
private void testMultipart(HttpServer httpServer, MediaType mediaType) throws Exception {
startServer(httpServer);
@SuppressWarnings("resource")
RequestEntity<MultiValueMap<String, Object>> request = RequestEntity
.post(URI.create("http://localhost:" + port + "/form-parts"))
.contentType(mediaType)
.body(generateBody());
ResponseEntity<Void> response = restTemplate.exchange(request, Void.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
private MultiValueMap<String, Object> generateBody() {
HttpHeaders fooHeaders = new HttpHeaders();
fooHeaders.setContentType(MediaType.TEXT_PLAIN);
ClassPathResource fooResource = new ClassPathResource("org/springframework/http/codec/multipart/foo.txt");
@ -102,7 +80,12 @@ class MultipartHttpHandlerIntegrationTests extends AbstractHttpHandlerIntegratio
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("fooPart", fooPart);
parts.add("barPart", barPart);
return parts;
URI url = URI.create("http://localhost:" + port + "/form-parts");
ResponseEntity<Void> response = new RestTemplate().exchange(
RequestEntity.post(url).contentType(mediaType).body(parts), Void.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}