diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/converter/MultipartAwareFormHttpMessageConverter.java b/spring-integration-http/src/main/java/org/springframework/integration/http/converter/MultipartAwareFormHttpMessageConverter.java
index 98a16f90d6e..bb8df00a2ca 100644
--- a/spring-integration-http/src/main/java/org/springframework/integration/http/converter/MultipartAwareFormHttpMessageConverter.java
+++ b/spring-integration-http/src/main/java/org/springframework/integration/http/converter/MultipartAwareFormHttpMessageConverter.java
@@ -18,14 +18,18 @@
import java.io.IOException;
import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
+import org.springframework.core.ResolvableType;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
+import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
@@ -39,9 +43,9 @@
import org.springframework.web.multipart.MultipartFile;
/**
- * An {@link HttpMessageConverter} implementation that delegates to an instance of
- * {@link MultipartHttpMessageConverter} while adding the capability to read
- * multipart/form-data content in an HTTP request.
+ * An {@link HttpMessageConverter} implementation that delegates to instances of
+ * {@link FormHttpMessageConverter} and {@link MultipartHttpMessageConverter} while adding
+ * the capability to read multipart/form-data content in an HTTP request.
*
* @author Mark Fisher
* @author Gary Russell
@@ -51,7 +55,11 @@
*/
public class MultipartAwareFormHttpMessageConverter implements HttpMessageConverter> {
- private final MultipartHttpMessageConverter wrappedConverter = new MultipartHttpMessageConverter();
+ private static final ResolvableType MULTI_VALUE_MAP_TYPE = ResolvableType.forClass(MultiValueMap.class);
+
+ private final FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
+
+ private final MultipartHttpMessageConverter multipartConverter = new MultipartHttpMessageConverter();
private MultipartFileReader> multipartFileReader = new DefaultMultipartFileReader();
@@ -60,7 +68,8 @@ public class MultipartAwareFormHttpMessageConverter implements HttpMessageConver
* @param charset The charset.
*/
public void setCharset(Charset charset) {
- this.wrappedConverter.setCharset(charset);
+ this.formConverter.setCharset(charset);
+ this.multipartConverter.setCharset(charset);
}
/**
@@ -74,7 +83,9 @@ public void setMultipartFileReader(MultipartFileReader> multipartFileReader) {
@Override
public List getSupportedMediaTypes() {
- return this.wrappedConverter.getSupportedMediaTypes();
+ List supportedMediaTypes = new ArrayList<>(this.formConverter.getSupportedMediaTypes());
+ supportedMediaTypes.addAll(this.multipartConverter.getSupportedMediaTypes());
+ return Collections.unmodifiableList(supportedMediaTypes);
}
@Override
@@ -93,16 +104,19 @@ public boolean canRead(Class> clazz, @Nullable MediaType mediaType) {
@Override
public boolean canWrite(Class> clazz, @Nullable MediaType mediaType) {
- return this.wrappedConverter.canWrite(clazz, mediaType);
+ return this.formConverter.canWrite(clazz, mediaType) || this.multipartConverter.canWrite(clazz, mediaType);
}
@Override
+ @SuppressWarnings("unchecked")
public MultiValueMap read(Class extends MultiValueMap> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
MediaType contentType = inputMessage.getHeaders().getContentType();
if (!MediaType.MULTIPART_FORM_DATA.includes(contentType)) {
- return this.wrappedConverter.read(clazz, inputMessage);
+ // The target type is always a MultiValueMap: a byte[] would make the delegate
+ // fall back to a single-value Map, which is not what this converter produces.
+ return (MultiValueMap) this.formConverter.read(MULTI_VALUE_MAP_TYPE, inputMessage, null);
}
Assert.state(inputMessage instanceof MultipartHttpInputMessage,
"A request with 'multipart/form-data' Content-Type must be a MultipartHttpInputMessage. "
@@ -130,7 +144,26 @@ public boolean canWrite(Class> clazz, @Nullable MediaType mediaType) {
public void write(MultiValueMap map, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
- this.wrappedConverter.write(map, contentType, outputMessage);
+ if (isMultipart(map, contentType)) {
+ this.multipartConverter.write(map, contentType, outputMessage);
+ }
+ else {
+ this.formConverter.write(map, contentType, outputMessage);
+ }
+ }
+
+ private static boolean isMultipart(MultiValueMap map, @Nullable MediaType contentType) {
+ if (contentType != null) {
+ return contentType.getType().equalsIgnoreCase("multipart");
+ }
+ for (List> values : map.values()) {
+ for (Object value : values) {
+ if (value != null && !(value instanceof String)) {
+ return true;
+ }
+ }
+ }
+ return false;
}
}
diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/converter/MultipartAwareFormHttpMessageConverterTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/converter/MultipartAwareFormHttpMessageConverterTests.java
new file mode 100644
index 00000000000..8e52734f309
--- /dev/null
+++ b/spring-integration-http/src/test/java/org/springframework/integration/http/converter/MultipartAwareFormHttpMessageConverterTests.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2026-present 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.integration.http.converter;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for the {@link MultipartAwareFormHttpMessageConverter}.
+ *
+ * @since 7.2
+ */
+public class MultipartAwareFormHttpMessageConverterTests {
+
+ private final MultipartAwareFormHttpMessageConverter converter = new MultipartAwareFormHttpMessageConverter();
+
+ @Test
+ public void supportedMediaTypesCoverFormAndMultipart() {
+ assertThat(this.converter.getSupportedMediaTypes())
+ .contains(MediaType.APPLICATION_FORM_URLENCODED, MediaType.MULTIPART_FORM_DATA);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void readsFormUrlEncodedBody() throws Exception {
+ MultiValueMap result =
+ (MultiValueMap) this.converter.read(
+ (Class extends MultiValueMap>) (Class>) LinkedMultiValueMap.class,
+ formInputMessage());
+
+ assertThat(result).containsOnlyKeys("name", "other");
+ assertThat(result.get("name")).containsExactly("foo", "bar");
+ }
+
+ @Test
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void readsFormUrlEncodedBodyForByteArrayTargetType() throws Exception {
+ // The inbound endpoint falls back to a 'byte[]' target type when no request payload type is configured.
+ Object result = ((HttpMessageConverter) this.converter).read(byte[].class, formInputMessage());
+
+ assertThat(result).isInstanceOf(MultiValueMap.class);
+ assertThat((MultiValueMap) result).containsOnlyKeys("name", "other");
+ }
+
+ @Test
+ public void writesFormUrlEncodedBody() throws Exception {
+ MultiValueMap form = new LinkedMultiValueMap<>();
+ form.add("name", "foo");
+ form.add("name", "bar");
+
+ ByteArrayOutputStream body = new ByteArrayOutputStream();
+ HttpHeaders headers = new HttpHeaders();
+ this.converter.write(form, MediaType.APPLICATION_FORM_URLENCODED, new HttpOutputMessage() {
+
+ @Override
+ public OutputStream getBody() {
+ return body;
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return headers;
+ }
+
+ });
+
+ MediaType contentType = headers.getContentType();
+ assertThat(contentType).isNotNull();
+ assertThat(contentType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)).isTrue();
+ assertThat(body.toString(StandardCharsets.UTF_8)).isEqualTo("name=foo&name=bar");
+ }
+
+ private static HttpInputMessage formInputMessage() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+ byte[] body = "name=foo&name=bar&other=baz".getBytes(StandardCharsets.UTF_8);
+ return new HttpInputMessage() {
+
+ @Override
+ public InputStream getBody() {
+ return new ByteArrayInputStream(body);
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return headers;
+ }
+
+ };
+ }
+
+}
diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java
index f0256c1587c..87d2f1988ab 100644
--- a/spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java
+++ b/spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java
@@ -23,7 +23,6 @@
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -126,7 +125,6 @@ public void setup() {
}
@Test
- @Disabled("See https://github.com/spring-projects/spring-integration/issues/11235")
public void testHttpProxyFlow() throws Exception {
RestTestClient restTestClient = RestTestClient.bindTo(this.mockMvc).build();
ClientHttpRequestFactory mockRequestFactory =