Skip to content

Commit 4799c3b

Browse files
artembilangaryrussell
authored andcommitted
GH-3711: Fix HTTP handler for content type header
Fixes #3711 The `contentType` header may come with parameter in its media type. * Fix `AbstractHttpRequestExecutingMessageHandler` to use `equalsTypeAndSubtype()` ignoring params * Some other code clean up in the `AbstractHttpRequestExecutingMessageHandler` * Ensure in the `HttpRequestExecutingMessageHandlerTests.simpleStringKeyStringValueFormData()` that provided `contentType` header is handled properly * Fix `HttpProxyScenarioTests.testHttpMultipartProxyScenario()` for mislead multi-part form handling **Cherry-pick to `5.5.x`**
1 parent 64d5b25 commit 4799c3b

File tree

4 files changed

+81
-72
lines changed

4 files changed

+81
-72
lines changed

spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2021 the original author or authors.
2+
* Copyright 2017-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -118,8 +118,8 @@ public AbstractHttpRequestExecutingMessageHandler(Expression uriExpression) {
118118

119119
/**
120120
* Set the encoding mode to use.
121-
* By default this is set to {@link DefaultUriBuilderFactory.EncodingMode#TEMPLATE_AND_VALUES}.
122-
* For more complicated scenarios consider to configure an {@link org.springframework.web.util.UriTemplateHandler}
121+
* By default, this is set to {@link DefaultUriBuilderFactory.EncodingMode#TEMPLATE_AND_VALUES}.
122+
* For more complicated scenarios consider configuring an {@link org.springframework.web.util.UriTemplateHandler}
123123
* on an externally provided {@link org.springframework.web.client.RestTemplate}.
124124
* @param encodingMode the mode to use for uri encoding
125125
* @since 5.3
@@ -151,7 +151,7 @@ public void setHttpMethod(HttpMethod httpMethod) {
151151
/**
152152
* Specify whether the outbound message's payload should be extracted
153153
* when preparing the request body.
154-
* Otherwise the Message instance itself is serialized.
154+
* Otherwise, the Message instance itself is serialized.
155155
* The default value is {@code true}.
156156
* @param extractPayload true if the payload should be extracted.
157157
*/
@@ -189,7 +189,7 @@ public void setExpectReply(boolean expectReply) {
189189

190190
/**
191191
* Specify the expected response type for the REST request.
192-
* Otherwise it is null and an empty {@link ResponseEntity} is returned from HTTP client.
192+
* Otherwise, it is null and an empty {@link ResponseEntity} is returned from HTTP client.
193193
* To take advantage of the HttpMessageConverters
194194
* registered on this adapter, provide a different type).
195195
* @param expectedResponseType The expected type.
@@ -378,8 +378,8 @@ private HttpEntity<?> createHttpEntityFromPayload(Message<?> message, HttpMethod
378378
: resolveContentType(payload);
379379
httpHeaders.setContentType(contentType);
380380
}
381-
if ((MediaType.APPLICATION_FORM_URLENCODED.equals(httpHeaders.getContentType()) ||
382-
MediaType.MULTIPART_FORM_DATA.equals(httpHeaders.getContentType()))
381+
if ((MediaType.APPLICATION_FORM_URLENCODED.equalsTypeAndSubtype(httpHeaders.getContentType()) ||
382+
MediaType.MULTIPART_FORM_DATA.equalsTypeAndSubtype(httpHeaders.getContentType()))
383383
&& !(payload instanceof MultiValueMap)) {
384384

385385
Assert.isInstanceOf(Map.class, payload,
@@ -471,8 +471,7 @@ private boolean isMultipart(Map<String, ?> map) {
471471
if (value.getClass().isArray()) {
472472
value = CollectionUtils.arrayToList(value);
473473
}
474-
if (value instanceof Collection) {
475-
Collection<?> cValues = (Collection<?>) value;
474+
if (value instanceof Collection<?> cValues) {
476475
for (Object cValue : cValues) {
477476
if (cValue != null && !(cValue instanceof String)) {
478477
return true;

spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests-context.xml

+14-17
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<beans:beans
3-
xmlns="http://www.springframework.org/schema/integration/http"
4-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5-
xmlns:beans="http://www.springframework.org/schema/beans"
6-
xmlns:int="http://www.springframework.org/schema/integration"
7-
xmlns:util="http://www.springframework.org/schema/util"
8-
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
3+
xmlns="http://www.springframework.org/schema/integration/http"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xmlns:beans="http://www.springframework.org/schema/beans"
6+
xmlns:int="http://www.springframework.org/schema/integration"
7+
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
98
http://www.springframework.org/schema/integration/http https://www.springframework.org/schema/integration/http/spring-integration-http.xsd
10-
http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
11-
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
9+
http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd">
1210

1311
<inbound-gateway path="/test" request-channel="testChannel"
1412
payload-expression="T(org.springframework.web.context.request.RequestContextHolder).requestAttributes.request.queryString"/>
@@ -25,15 +23,14 @@
2523
</int:channel>
2624

2725
<inbound-gateway path="/testmp" request-channel="testChannelmp"
28-
merge-with-default-converters="false"
29-
message-converters="converters"
30-
request-payload-type="byte[]" />
31-
32-
<util:list id="converters">
33-
<beans:bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
34-
<beans:bean class="org.springframework.http.converter.StringHttpMessageConverter" />
35-
<beans:bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
36-
</util:list>
26+
message-converters="formHttpMessageConverter"/>
27+
28+
<beans:bean id="formHttpMessageConverter"
29+
class="org.springframework.integration.http.converter.MultipartAwareFormHttpMessageConverter">
30+
<beans:property name="multipartFileReader">
31+
<beans:bean class="org.springframework.integration.http.multipart.SimpleMultipartFileReader"/>
32+
</beans:property>
33+
</beans:bean>
3734

3835
<int:channel id="testChannelmp"/>
3936

spring-integration-http/src/test/java/org/springframework/integration/http/HttpProxyScenarioTests.java

+40-36
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2020 the original author or authors.
2+
* Copyright 2013-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,6 +46,7 @@
4646
import org.springframework.mock.web.MockHttpServletResponse;
4747
import org.springframework.test.annotation.DirtiesContext;
4848
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
49+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
4950
import org.springframework.util.LinkedMultiValueMap;
5051
import org.springframework.util.MultiValueMap;
5152
import org.springframework.web.client.RestTemplate;
@@ -120,19 +121,19 @@ public void testHttpProxyScenario() throws Exception {
120121
final String contentDispositionValue = "attachment; filename=\"test.txt\"";
121122

122123
Mockito.doAnswer(invocation -> {
123-
String uri = invocation.getArgument(0);
124-
assertThat(uri).isEqualTo("http://testServer/test?foo=bar&FOO=BAR");
125-
HttpEntity<?> httpEntity = (HttpEntity<?>) invocation.getArguments()[2];
126-
HttpHeaders httpHeaders = httpEntity.getHeaders();
127-
assertThat(httpHeaders.getIfModifiedSince()).isEqualTo(ifModifiedSince);
128-
assertThat(httpHeaders.getFirst("If-Unmodified-Since")).isEqualTo(ifUnmodifiedSinceValue);
129-
assertThat(httpHeaders.getFirst("Connection")).isEqualTo("Keep-Alive");
130-
131-
MultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>(httpHeaders);
132-
responseHeaders.set("Connection", "close");
133-
responseHeaders.set("Content-Disposition", contentDispositionValue);
134-
return new ResponseEntity<>(responseHeaders, HttpStatus.OK);
135-
}).when(template)
124+
String uri = invocation.getArgument(0);
125+
assertThat(uri).isEqualTo("http://testServer/test?foo=bar&FOO=BAR");
126+
HttpEntity<?> httpEntity = (HttpEntity<?>) invocation.getArguments()[2];
127+
HttpHeaders httpHeaders = httpEntity.getHeaders();
128+
assertThat(httpHeaders.getIfModifiedSince()).isEqualTo(ifModifiedSince);
129+
assertThat(httpHeaders.getFirst("If-Unmodified-Since")).isEqualTo(ifUnmodifiedSinceValue);
130+
assertThat(httpHeaders.getFirst("Connection")).isEqualTo("Keep-Alive");
131+
132+
MultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>(httpHeaders);
133+
responseHeaders.set("Connection", "close");
134+
responseHeaders.set("Content-Disposition", contentDispositionValue);
135+
return new ResponseEntity<>(responseHeaders, HttpStatus.OK);
136+
}).when(template)
136137
.exchange(Mockito.anyString(), Mockito.any(HttpMethod.class),
137138
Mockito.any(HttpEntity.class), (Class<?>) isNull(), Mockito.anyMap());
138139

@@ -160,12 +161,14 @@ public void testHttpProxyScenario() throws Exception {
160161
}
161162

162163
@Test
164+
@SuppressWarnings("unchecked")
163165
public void testHttpMultipartProxyScenario() throws Exception {
164-
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/testmp");
165-
166-
request.addHeader("Connection", "Keep-Alive");
167-
request.setContentType("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ");
168-
request.setContent("foo".getBytes());
166+
MockHttpServletRequest request =
167+
MockMvcRequestBuilders.multipart("/testmp")
168+
.file("foo", "foo".getBytes())
169+
.contentType("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ")
170+
.header("Connection", "Keep-Alive")
171+
.buildRequest(null);
169172

170173
Object handler = this.handlerMapping.getHandler(request).getHandler();
171174
assertThat(handler).isNotNull();
@@ -174,23 +177,24 @@ public void testHttpMultipartProxyScenario() throws Exception {
174177

175178
RestTemplate template = Mockito.spy(new RestTemplate());
176179
Mockito.doAnswer(invocation -> {
177-
String uri = invocation.getArgument(0);
178-
assertThat(uri).isEqualTo("http://testServer/testmp");
179-
HttpEntity<?> httpEntity = (HttpEntity<?>) invocation.getArguments()[2];
180-
HttpHeaders httpHeaders = httpEntity.getHeaders();
181-
assertThat(httpHeaders.getFirst("Connection")).isEqualTo("Keep-Alive");
182-
assertThat(httpHeaders.getContentType().toString())
183-
.isEqualTo("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ");
184-
185-
HttpEntity<?> entity = (HttpEntity<?>) invocation.getArguments()[2];
186-
assertThat(entity.getBody()).isInstanceOf(byte[].class);
187-
assertThat(new String((byte[]) entity.getBody())).isEqualTo("foo");
188-
189-
MultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>(httpHeaders);
190-
responseHeaders.set("Connection", "close");
191-
responseHeaders.set("Content-Type", "text/plain");
192-
return new ResponseEntity<>(responseHeaders, HttpStatus.OK);
193-
}).when(template)
180+
String uri = invocation.getArgument(0);
181+
assertThat(uri).isEqualTo("http://testServer/testmp");
182+
HttpEntity<?> httpEntity = (HttpEntity<?>) invocation.getArguments()[2];
183+
HttpHeaders httpHeaders = httpEntity.getHeaders();
184+
assertThat(httpHeaders.getFirst("Connection")).isEqualTo("Keep-Alive");
185+
assertThat(httpHeaders.getContentType().toString())
186+
.isEqualTo("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ");
187+
188+
HttpEntity<?> entity = (HttpEntity<?>) invocation.getArguments()[2];
189+
assertThat(entity.getBody()).isInstanceOf(MultiValueMap.class);
190+
assertThat(((MultiValueMap<String, ?>) entity.getBody()).getFirst("foo"))
191+
.isEqualTo("foo".getBytes());
192+
193+
MultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>(httpHeaders);
194+
responseHeaders.set("Connection", "close");
195+
responseHeaders.set("Content-Type", "text/plain");
196+
return new ResponseEntity<>(responseHeaders, HttpStatus.OK);
197+
}).when(template)
194198
.exchange(Mockito.anyString(), Mockito.any(HttpMethod.class),
195199
Mockito.any(HttpEntity.class), (Class<?>) isNull(), Mockito.anyMap());
196200

spring-integration-http/src/test/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandlerTests.java

+19-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -61,6 +61,7 @@
6161
import org.springframework.lang.Nullable;
6262
import org.springframework.messaging.Message;
6363
import org.springframework.messaging.MessageChannel;
64+
import org.springframework.messaging.MessageHeaders;
6465
import org.springframework.messaging.PollableChannel;
6566
import org.springframework.messaging.support.GenericMessage;
6667
import org.springframework.mock.http.client.MockClientHttpResponse;
@@ -85,10 +86,12 @@
8586
*/
8687
public class HttpRequestExecutingMessageHandlerTests {
8788

89+
// Used in the HttpOutboundWithinChainTests-context.xml
8890
public static ParameterizedTypeReference<List<String>> testParameterizedTypeReference() {
89-
return new ParameterizedTypeReference<List<String>>() {
91+
return new ParameterizedTypeReference<>() {
9092

9193
};
94+
9295
}
9396

9497
@Test
@@ -104,7 +107,11 @@ public void simpleStringKeyStringValueFormData() {
104107
form.put("a", "1");
105108
form.put("b", "2");
106109
form.put("c", "3");
107-
Message<?> message = MessageBuilder.withPayload(form).build();
110+
Message<?> message =
111+
MessageBuilder.withPayload(form)
112+
.setHeader(MessageHeaders.CONTENT_TYPE,
113+
MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8")
114+
.build();
108115
QueueChannel replyChannel = new QueueChannel();
109116
handler.setOutputChannel(replyChannel);
110117

@@ -115,12 +122,14 @@ public void simpleStringKeyStringValueFormData() {
115122
HttpEntity<?> request = template.lastRequestEntity.get();
116123
Object body = request.getBody();
117124
assertThat(request.getHeaders().getContentType()).isNotNull();
118-
assertThat(body instanceof MultiValueMap<?, ?>).isTrue();
125+
assertThat(body).isInstanceOf(MultiValueMap.class);
119126
MultiValueMap<?, ?> map = (MultiValueMap<?, ?>) body;
120127
assertThat(map.get("a").iterator().next()).isEqualTo("1");
121128
assertThat(map.get("b").iterator().next()).isEqualTo("2");
122129
assertThat(map.get("c").iterator().next()).isEqualTo("3");
123-
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED);
130+
assertThat(request.getHeaders().getContentType()).isNotEqualTo(MediaType.APPLICATION_FORM_URLENCODED);
131+
assertThat(request.getHeaders().getContentType().equalsTypeAndSubtype(MediaType.APPLICATION_FORM_URLENCODED))
132+
.isTrue();
124133
}
125134

126135
@Test
@@ -541,7 +550,7 @@ public void contentAsByteArray() {
541550

542551
HttpEntity<?> request = template.lastRequestEntity.get();
543552
Object body = request.getBody();
544-
assertThat(body instanceof byte[]).isTrue();
553+
assertThat(body).isInstanceOf(byte[].class);
545554
assertThat(new String(bytes)).isEqualTo("Hello World");
546555
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_OCTET_STREAM);
547556
}
@@ -564,7 +573,7 @@ public void contentAsXmlSource() {
564573

565574
HttpEntity<?> request = template.lastRequestEntity.get();
566575
Object body = request.getBody();
567-
assertThat(body instanceof Source).isTrue();
576+
assertThat(body).isInstanceOf(Source.class);
568577
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_XML);
569578
}
570579

@@ -784,7 +793,7 @@ public void acceptHeaderForSerializableResponse() throws IOException {
784793
assertThat(requestHeaders.getAccept()).isNotNull();
785794
assertThat(requestHeaders.getAccept().size() > 0).isTrue();
786795
List<MediaType> accept = requestHeaders.getAccept();
787-
assertThat(accept.size() > 0).isTrue();
796+
assertThat(accept).hasSizeGreaterThan(0);
788797
assertThat(accept.get(0).getType()).isEqualTo("application");
789798
assertThat(accept.get(0).getSubtype()).isEqualTo("x-java-serialized-object");
790799
}
@@ -815,13 +824,13 @@ public void acceptHeaderForSerializableResponseMessageExchange() throws IOExcept
815824
assertThat(requestHeaders.getAccept()).isNotNull();
816825
assertThat(requestHeaders.getAccept().size() > 0).isTrue();
817826
List<MediaType> accept = requestHeaders.getAccept();
818-
assertThat(accept.size() > 0).isTrue();
827+
assertThat(accept).hasSizeGreaterThan(0);
819828
assertThat(accept.get(0).getType()).isEqualTo("application");
820829
assertThat(accept.get(0).getSubtype()).isEqualTo("x-java-serialized-object");
821830
}
822831

823832
@Test
824-
public void testNoContentTypeAndSmartConverter() throws IOException {
833+
public void testNoContentTypeAndSmartConverter() {
825834
Sinks.One<HttpHeaders> httpHeadersSink = Sinks.one();
826835
RestTemplate testRestTemplate = new RestTemplate() {
827836
@Nullable

0 commit comments

Comments
 (0)