Refine multipart converter registration in HttpMessageConverters

As of #33894, `HttpMessageConverters` auto-detects converters and use
custom-provided ones to configure a collection of converters for the
client or the server.
Right now the multipart converter is only configured if core converters
(JSON, XML...) are configured/detected. We do not reuse the base
converters (resource, string, byte array) for the multipart converter
as it applies different encoding defaults (ISO for the main ones, UTF-8
for multipart).

This commit refines the configuration to not only include the multipart
converter when core converters are present, but also if any other
converter was configured.

Closes gh-35203
This commit is contained in:
Brian Clozel
2025-07-15 14:45:28 +02:00
parent 2e0cc63616
commit ad007caebc
3 changed files with 34 additions and 11 deletions
@@ -17,8 +17,6 @@
package org.springframework.http.converter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
@@ -110,7 +108,9 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
@Nullable HttpMessageConverter<?> stringMessageConverter;
List<HttpMessageConverter<?>> resourceMessageConverters = Collections.emptyList();
@Nullable HttpMessageConverter<?> resourceMessageConverter;
@Nullable HttpMessageConverter<?> resourceRegionMessageConverter;
@Nullable Consumer<HttpMessageConverter<?>> configurer;
@@ -386,7 +386,7 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
@Override
public HttpMessageConverters build() {
if (this.registerDefaults) {
this.resourceMessageConverters = Collections.singletonList(new ResourceHttpMessageConverter(false));
this.resourceMessageConverter = new ResourceHttpMessageConverter(false);
detectMessageConverters();
}
List<HttpMessageConverter<?>> allConverters = new ArrayList<>();
@@ -396,8 +396,10 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
allConverters.addAll(this.getCustomConverters());
allConverters.addAll(this.getBaseConverters());
allConverters.addAll(this.resourceMessageConverters);
if (!partConverters.isEmpty()) {
if (this.resourceMessageConverter != null) {
allConverters.add(this.resourceMessageConverter);
}
if (!partConverters.isEmpty() || !allConverters.isEmpty()) {
allConverters.add(new AllEncompassingFormHttpMessageConverter(partConverters));
}
allConverters.addAll(this.getCoreConverters());
@@ -468,7 +470,8 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
@Override
public HttpMessageConverters build() {
if (this.registerDefaults) {
this.resourceMessageConverters = Arrays.asList(new ResourceHttpMessageConverter(), new ResourceRegionHttpMessageConverter());
this.resourceMessageConverter = new ResourceHttpMessageConverter();
this.resourceRegionMessageConverter = new ResourceRegionHttpMessageConverter();
detectMessageConverters();
}
List<HttpMessageConverter<?>> allConverters = new ArrayList<>();
@@ -479,8 +482,13 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
allConverters.addAll(this.getCustomConverters());
allConverters.addAll(this.getBaseConverters());
allConverters.addAll(this.resourceMessageConverters);
if (!partConverters.isEmpty()) {
if (this.resourceMessageConverter != null) {
allConverters.add(this.resourceMessageConverter);
}
if (this.resourceRegionMessageConverter != null) {
allConverters.add(this.resourceRegionMessageConverter);
}
if (!partConverters.isEmpty() || !allConverters.isEmpty()) {
allConverters.add(new AllEncompassingFormHttpMessageConverter(partConverters));
}
allConverters.addAll(this.getCoreConverters());
@@ -160,6 +160,13 @@ class DefaultHttpMessageConvertersTests {
assertThat(multipartConverter.getPartConverters()).hasAtLeastOneElementOfType(CustomHttpMessageConverter.class);
}
@Test
void registerMultipartConverterWhenOtherConvertersPresent() {
var converters = HttpMessageConverters.forClient()
.stringMessageConverter(new StringHttpMessageConverter()).build();
assertThat(converters).hasExactlyElementsOfTypes(StringHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class);
}
@Test
void shouldUseSpecificConverter() {
var jacksonConverter = new JacksonJsonHttpMessageConverter();
@@ -248,7 +255,14 @@ class DefaultHttpMessageConvertersTests {
}
@Test
void shouldUseServerSpecificConverter() {
void registerMultipartConverterWhenOtherConvertersPresent() {
var converters = HttpMessageConverters.forServer()
.stringMessageConverter(new StringHttpMessageConverter()).build();
assertThat(converters).hasExactlyElementsOfTypes(StringHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class);
}
@Test
void shouldUseSpecificConverter() {
var jacksonConverter = new JacksonJsonHttpMessageConverter();
var converters = HttpMessageConverters.forServer().registerDefaults()
.jsonMessageConverter(jacksonConverter).build();
@@ -33,6 +33,7 @@ import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.web.util.DefaultUriBuilderFactory;
import static org.assertj.core.api.Assertions.assertThat;
@@ -150,7 +151,7 @@ public class RestClientBuilderTests {
assertThat(fieldValue("messageConverters", defaultBuilder))
.asInstanceOf(InstanceOfAssertFactories.LIST)
.containsExactly(stringConverter);
.hasExactlyElementsOfTypes(StringHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class);
}
@Test