diff --git a/framework-docs/src/docs/asciidoc/web/webflux.adoc b/framework-docs/src/docs/asciidoc/web/webflux.adoc index 514f2cb7e73..867001cab8f 100644 --- a/framework-docs/src/docs/asciidoc/web/webflux.adoc +++ b/framework-docs/src/docs/asciidoc/web/webflux.adoc @@ -614,7 +614,7 @@ The `DefaultServerWebExchange` uses the configured `HttpMessageReader` to parse The `DefaultServerWebExchange` uses the configured `HttpMessageReader>` 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` and then simply collects the parts into a `MultiValueMap`. By default, the `DefaultPartHttpMessageReader` is used, but this can be changed through the diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index 87f576988c9..e95062943af 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -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> initFormData(ServerHttpRequest request, ServerCodecConfigurer configurer, String logPrefix) { - try { - MediaType contentType = request.getHeaders().getContentType(); - if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) { - return ((HttpMessageReader>) 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> 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> initMultipartData(ServerHttpRequest request, ServerCodecConfigurer configurer, String logPrefix) { + MediaType contentType = getContentType(request); + if (contentType == null || !contentType.getType().equalsIgnoreCase("multipart")) { + return EMPTY_MULTIPART_DATA; + } + + HttpMessageReader> 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>) 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 contentType; + } + + @SuppressWarnings("unchecked") + @Nullable + private static HttpMessageReader getReader( + ServerCodecConfigurer configurer, MediaType contentType, ResolvableType targetType) { + + HttpMessageReader result = null; + for (HttpMessageReader reader : configurer.getReaders()) { + if (reader.canRead(targetType, contentType)) { + result = (HttpMessageReader) reader; + } } - return EMPTY_MULTIPART_DATA; + return result; } diff --git a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java index 8f65b32e05d..5fbe50d5811 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java @@ -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 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); } diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/MultipartHttpHandlerIntegrationTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/MultipartHttpHandlerIntegrationTests.java index 62fb1c7db28..6d8d81b89a1 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/MultipartHttpHandlerIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/MultipartHttpHandlerIntegrationTests.java @@ -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> request = RequestEntity - .post(URI.create("http://localhost:" + port + "/form-parts")) - .contentType(mediaType) - .body(generateBody()); - ResponseEntity response = restTemplate.exchange(request, Void.class); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - } - - private MultiValueMap 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 parts = new LinkedMultiValueMap<>(); parts.add("fooPart", fooPart); parts.add("barPart", barPart); - return parts; + + URI url = URI.create("http://localhost:" + port + "/form-parts"); + ResponseEntity response = new RestTemplate().exchange( + RequestEntity.post(url).contentType(mediaType).body(parts), Void.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); }