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:
parent
67df0756cd
commit
f5c1e2ffa1
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue