GH-3711: Fix HTTP handler for content type header
Fixes https://github.com/spring-projects/spring-integration/issues/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`**
This commit is contained in:
committed by
Gary Russell
parent
413d66018e
commit
25e763d683
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017-2021 the original author or authors.
|
||||
* Copyright 2017-2022 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.
|
||||
@@ -118,8 +118,8 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
|
||||
|
||||
/**
|
||||
* Set the encoding mode to use.
|
||||
* By default this is set to {@link DefaultUriBuilderFactory.EncodingMode#TEMPLATE_AND_VALUES}.
|
||||
* For more complicated scenarios consider to configure an {@link org.springframework.web.util.UriTemplateHandler}
|
||||
* By default, this is set to {@link DefaultUriBuilderFactory.EncodingMode#TEMPLATE_AND_VALUES}.
|
||||
* For more complicated scenarios consider configuring an {@link org.springframework.web.util.UriTemplateHandler}
|
||||
* on an externally provided {@link org.springframework.web.client.RestTemplate}.
|
||||
* @param encodingMode the mode to use for uri encoding
|
||||
* @since 5.3
|
||||
@@ -151,7 +151,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
|
||||
/**
|
||||
* Specify whether the outbound message's payload should be extracted
|
||||
* when preparing the request body.
|
||||
* Otherwise the Message instance itself is serialized.
|
||||
* Otherwise, the Message instance itself is serialized.
|
||||
* The default value is {@code true}.
|
||||
* @param extractPayload true if the payload should be extracted.
|
||||
*/
|
||||
@@ -189,7 +189,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
|
||||
|
||||
/**
|
||||
* Specify the expected response type for the REST request.
|
||||
* Otherwise it is null and an empty {@link ResponseEntity} is returned from HTTP client.
|
||||
* Otherwise, it is null and an empty {@link ResponseEntity} is returned from HTTP client.
|
||||
* To take advantage of the HttpMessageConverters
|
||||
* registered on this adapter, provide a different type).
|
||||
* @param expectedResponseType The expected type.
|
||||
@@ -378,8 +378,8 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
|
||||
: resolveContentType(payload);
|
||||
httpHeaders.setContentType(contentType);
|
||||
}
|
||||
if ((MediaType.APPLICATION_FORM_URLENCODED.equals(httpHeaders.getContentType()) ||
|
||||
MediaType.MULTIPART_FORM_DATA.equals(httpHeaders.getContentType()))
|
||||
if ((MediaType.APPLICATION_FORM_URLENCODED.equalsTypeAndSubtype(httpHeaders.getContentType()) ||
|
||||
MediaType.MULTIPART_FORM_DATA.equalsTypeAndSubtype(httpHeaders.getContentType()))
|
||||
&& !(payload instanceof MultiValueMap)) {
|
||||
|
||||
Assert.isInstanceOf(Map.class, payload,
|
||||
@@ -471,8 +471,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
|
||||
if (value.getClass().isArray()) {
|
||||
value = CollectionUtils.arrayToList(value);
|
||||
}
|
||||
if (value instanceof Collection) {
|
||||
Collection<?> cValues = (Collection<?>) value;
|
||||
if (value instanceof Collection<?> cValues) {
|
||||
for (Object cValue : cValues) {
|
||||
if (cValue != null && !(cValue instanceof String)) {
|
||||
return true;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans:beans
|
||||
xmlns="http://www.springframework.org/schema/integration/http"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:int="http://www.springframework.org/schema/integration"
|
||||
xmlns:util="http://www.springframework.org/schema/util"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
xmlns="http://www.springframework.org/schema/integration/http"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:int="http://www.springframework.org/schema/integration"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/http https://www.springframework.org/schema/integration/http/spring-integration-http.xsd
|
||||
http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
|
||||
http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
<inbound-gateway path="/test" request-channel="testChannel"
|
||||
payload-expression="T(org.springframework.web.context.request.RequestContextHolder).requestAttributes.request.queryString"/>
|
||||
@@ -25,15 +23,14 @@
|
||||
</int:channel>
|
||||
|
||||
<inbound-gateway path="/testmp" request-channel="testChannelmp"
|
||||
merge-with-default-converters="false"
|
||||
message-converters="converters"
|
||||
request-payload-type="byte[]" />
|
||||
message-converters="formHttpMessageConverter"/>
|
||||
|
||||
<util:list id="converters">
|
||||
<beans:bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
|
||||
<beans:bean class="org.springframework.http.converter.StringHttpMessageConverter" />
|
||||
<beans:bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
|
||||
</util:list>
|
||||
<beans:bean id="formHttpMessageConverter"
|
||||
class="org.springframework.integration.http.converter.MultipartAwareFormHttpMessageConverter">
|
||||
<beans:property name="multipartFileReader">
|
||||
<beans:bean class="org.springframework.integration.http.multipart.SimpleMultipartFileReader"/>
|
||||
</beans:property>
|
||||
</beans:bean>
|
||||
|
||||
<int:channel id="testChannelmp"/>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2022 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.
|
||||
@@ -46,6 +46,7 @@ import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
@@ -120,19 +121,19 @@ public class HttpProxyScenarioTests {
|
||||
final String contentDispositionValue = "attachment; filename=\"test.txt\"";
|
||||
|
||||
Mockito.doAnswer(invocation -> {
|
||||
String uri = invocation.getArgument(0);
|
||||
assertThat(uri).isEqualTo("http://testServer/test?foo=bar&FOO=BAR");
|
||||
HttpEntity<?> httpEntity = (HttpEntity<?>) invocation.getArguments()[2];
|
||||
HttpHeaders httpHeaders = httpEntity.getHeaders();
|
||||
assertThat(httpHeaders.getIfModifiedSince()).isEqualTo(ifModifiedSince);
|
||||
assertThat(httpHeaders.getFirst("If-Unmodified-Since")).isEqualTo(ifUnmodifiedSinceValue);
|
||||
assertThat(httpHeaders.getFirst("Connection")).isEqualTo("Keep-Alive");
|
||||
String uri = invocation.getArgument(0);
|
||||
assertThat(uri).isEqualTo("http://testServer/test?foo=bar&FOO=BAR");
|
||||
HttpEntity<?> httpEntity = (HttpEntity<?>) invocation.getArguments()[2];
|
||||
HttpHeaders httpHeaders = httpEntity.getHeaders();
|
||||
assertThat(httpHeaders.getIfModifiedSince()).isEqualTo(ifModifiedSince);
|
||||
assertThat(httpHeaders.getFirst("If-Unmodified-Since")).isEqualTo(ifUnmodifiedSinceValue);
|
||||
assertThat(httpHeaders.getFirst("Connection")).isEqualTo("Keep-Alive");
|
||||
|
||||
MultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>(httpHeaders);
|
||||
responseHeaders.set("Connection", "close");
|
||||
responseHeaders.set("Content-Disposition", contentDispositionValue);
|
||||
return new ResponseEntity<>(responseHeaders, HttpStatus.OK);
|
||||
}).when(template)
|
||||
MultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>(httpHeaders);
|
||||
responseHeaders.set("Connection", "close");
|
||||
responseHeaders.set("Content-Disposition", contentDispositionValue);
|
||||
return new ResponseEntity<>(responseHeaders, HttpStatus.OK);
|
||||
}).when(template)
|
||||
.exchange(Mockito.anyString(), Mockito.any(HttpMethod.class),
|
||||
Mockito.any(HttpEntity.class), (Class<?>) isNull(), Mockito.anyMap());
|
||||
|
||||
@@ -160,12 +161,14 @@ public class HttpProxyScenarioTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testHttpMultipartProxyScenario() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/testmp");
|
||||
|
||||
request.addHeader("Connection", "Keep-Alive");
|
||||
request.setContentType("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ");
|
||||
request.setContent("foo".getBytes());
|
||||
MockHttpServletRequest request =
|
||||
MockMvcRequestBuilders.multipart("/testmp")
|
||||
.file("foo", "foo".getBytes())
|
||||
.contentType("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ")
|
||||
.header("Connection", "Keep-Alive")
|
||||
.buildRequest(null);
|
||||
|
||||
Object handler = this.handlerMapping.getHandler(request).getHandler();
|
||||
assertThat(handler).isNotNull();
|
||||
@@ -174,23 +177,24 @@ public class HttpProxyScenarioTests {
|
||||
|
||||
RestTemplate template = Mockito.spy(new RestTemplate());
|
||||
Mockito.doAnswer(invocation -> {
|
||||
String uri = invocation.getArgument(0);
|
||||
assertThat(uri).isEqualTo("http://testServer/testmp");
|
||||
HttpEntity<?> httpEntity = (HttpEntity<?>) invocation.getArguments()[2];
|
||||
HttpHeaders httpHeaders = httpEntity.getHeaders();
|
||||
assertThat(httpHeaders.getFirst("Connection")).isEqualTo("Keep-Alive");
|
||||
assertThat(httpHeaders.getContentType().toString())
|
||||
.isEqualTo("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ");
|
||||
String uri = invocation.getArgument(0);
|
||||
assertThat(uri).isEqualTo("http://testServer/testmp");
|
||||
HttpEntity<?> httpEntity = (HttpEntity<?>) invocation.getArguments()[2];
|
||||
HttpHeaders httpHeaders = httpEntity.getHeaders();
|
||||
assertThat(httpHeaders.getFirst("Connection")).isEqualTo("Keep-Alive");
|
||||
assertThat(httpHeaders.getContentType().toString())
|
||||
.isEqualTo("multipart/form-data;boundary=----WebKitFormBoundarywABD2xqC1FLBijlQ");
|
||||
|
||||
HttpEntity<?> entity = (HttpEntity<?>) invocation.getArguments()[2];
|
||||
assertThat(entity.getBody()).isInstanceOf(byte[].class);
|
||||
assertThat(new String((byte[]) entity.getBody())).isEqualTo("foo");
|
||||
HttpEntity<?> entity = (HttpEntity<?>) invocation.getArguments()[2];
|
||||
assertThat(entity.getBody()).isInstanceOf(MultiValueMap.class);
|
||||
assertThat(((MultiValueMap<String, ?>) entity.getBody()).getFirst("foo"))
|
||||
.isEqualTo("foo".getBytes());
|
||||
|
||||
MultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>(httpHeaders);
|
||||
responseHeaders.set("Connection", "close");
|
||||
responseHeaders.set("Content-Type", "text/plain");
|
||||
return new ResponseEntity<>(responseHeaders, HttpStatus.OK);
|
||||
}).when(template)
|
||||
MultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>(httpHeaders);
|
||||
responseHeaders.set("Connection", "close");
|
||||
responseHeaders.set("Content-Type", "text/plain");
|
||||
return new ResponseEntity<>(responseHeaders, HttpStatus.OK);
|
||||
}).when(template)
|
||||
.exchange(Mockito.anyString(), Mockito.any(HttpMethod.class),
|
||||
Mockito.any(HttpEntity.class), (Class<?>) isNull(), Mockito.anyMap());
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -61,6 +61,7 @@ import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
@@ -85,10 +86,12 @@ import reactor.test.StepVerifier;
|
||||
*/
|
||||
public class HttpRequestExecutingMessageHandlerTests {
|
||||
|
||||
// Used in the HttpOutboundWithinChainTests-context.xml
|
||||
public static ParameterizedTypeReference<List<String>> testParameterizedTypeReference() {
|
||||
return new ParameterizedTypeReference<List<String>>() {
|
||||
return new ParameterizedTypeReference<>() {
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -104,7 +107,11 @@ public class HttpRequestExecutingMessageHandlerTests {
|
||||
form.put("a", "1");
|
||||
form.put("b", "2");
|
||||
form.put("c", "3");
|
||||
Message<?> message = MessageBuilder.withPayload(form).build();
|
||||
Message<?> message =
|
||||
MessageBuilder.withPayload(form)
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE,
|
||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8")
|
||||
.build();
|
||||
QueueChannel replyChannel = new QueueChannel();
|
||||
handler.setOutputChannel(replyChannel);
|
||||
|
||||
@@ -115,12 +122,14 @@ public class HttpRequestExecutingMessageHandlerTests {
|
||||
HttpEntity<?> request = template.lastRequestEntity.get();
|
||||
Object body = request.getBody();
|
||||
assertThat(request.getHeaders().getContentType()).isNotNull();
|
||||
assertThat(body instanceof MultiValueMap<?, ?>).isTrue();
|
||||
assertThat(body).isInstanceOf(MultiValueMap.class);
|
||||
MultiValueMap<?, ?> map = (MultiValueMap<?, ?>) body;
|
||||
assertThat(map.get("a").iterator().next()).isEqualTo("1");
|
||||
assertThat(map.get("b").iterator().next()).isEqualTo("2");
|
||||
assertThat(map.get("c").iterator().next()).isEqualTo("3");
|
||||
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
assertThat(request.getHeaders().getContentType()).isNotEqualTo(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
assertThat(request.getHeaders().getContentType().equalsTypeAndSubtype(MediaType.APPLICATION_FORM_URLENCODED))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -541,7 +550,7 @@ public class HttpRequestExecutingMessageHandlerTests {
|
||||
|
||||
HttpEntity<?> request = template.lastRequestEntity.get();
|
||||
Object body = request.getBody();
|
||||
assertThat(body instanceof byte[]).isTrue();
|
||||
assertThat(body).isInstanceOf(byte[].class);
|
||||
assertThat(new String(bytes)).isEqualTo("Hello World");
|
||||
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
@@ -564,7 +573,7 @@ public class HttpRequestExecutingMessageHandlerTests {
|
||||
|
||||
HttpEntity<?> request = template.lastRequestEntity.get();
|
||||
Object body = request.getBody();
|
||||
assertThat(body instanceof Source).isTrue();
|
||||
assertThat(body).isInstanceOf(Source.class);
|
||||
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_XML);
|
||||
}
|
||||
|
||||
@@ -784,7 +793,7 @@ public class HttpRequestExecutingMessageHandlerTests {
|
||||
assertThat(requestHeaders.getAccept()).isNotNull();
|
||||
assertThat(requestHeaders.getAccept().size() > 0).isTrue();
|
||||
List<MediaType> accept = requestHeaders.getAccept();
|
||||
assertThat(accept.size() > 0).isTrue();
|
||||
assertThat(accept).hasSizeGreaterThan(0);
|
||||
assertThat(accept.get(0).getType()).isEqualTo("application");
|
||||
assertThat(accept.get(0).getSubtype()).isEqualTo("x-java-serialized-object");
|
||||
}
|
||||
@@ -815,13 +824,13 @@ public class HttpRequestExecutingMessageHandlerTests {
|
||||
assertThat(requestHeaders.getAccept()).isNotNull();
|
||||
assertThat(requestHeaders.getAccept().size() > 0).isTrue();
|
||||
List<MediaType> accept = requestHeaders.getAccept();
|
||||
assertThat(accept.size() > 0).isTrue();
|
||||
assertThat(accept).hasSizeGreaterThan(0);
|
||||
assertThat(accept.get(0).getType()).isEqualTo("application");
|
||||
assertThat(accept.get(0).getSubtype()).isEqualTo("x-java-serialized-object");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoContentTypeAndSmartConverter() throws IOException {
|
||||
public void testNoContentTypeAndSmartConverter() {
|
||||
Sinks.One<HttpHeaders> httpHeadersSink = Sinks.one();
|
||||
RestTemplate testRestTemplate = new RestTemplate() {
|
||||
@Nullable
|
||||
|
||||
Reference in New Issue
Block a user