Skip to content

Commit a3017c0

Browse files
artembilanspring-builds
authored andcommitted
GH-9489: Remove Content-Length HTTP before sending GET request
Fixes: #9489 Issue link: #9489 If request message has a `Content-Length` HTTP, it is still mapped to the target HTTP request even if that one is indicated as "no-body" (`GET`, `HEAD`, `TRACE`). In this case Netty fails to decode such a missed body with error: ``` java.lang.IllegalArgumentException: text is empty (possibly HTTP/0.9)), version: HTTP/1.0 ``` * Since `Content-Length` is not supposed to be supported for those methods, remove it altogether from the HTTP request headers * Add nullability API into the `org.springframework.integration.http.outbound` * Check received HTTP request on the server side that it does not have such a header for `GET` (cherry picked from commit 891dca7)
1 parent 8bacb75 commit a3017c0

File tree

4 files changed

+55
-10
lines changed

4 files changed

+55
-10
lines changed

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2023 the original author or authors.
2+
* Copyright 2017-2024 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.
@@ -32,7 +32,6 @@
3232
import javax.xml.transform.Source;
3333

3434
import org.springframework.beans.factory.BeanFactory;
35-
import org.springframework.context.ApplicationContext;
3635
import org.springframework.core.ParameterizedTypeReference;
3736
import org.springframework.expression.EvaluationContext;
3837
import org.springframework.expression.Expression;
@@ -100,6 +99,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
10099

101100
private boolean expectReply = true;
102101

102+
@Nullable
103103
private Expression expectedResponseTypeExpression;
104104

105105
private boolean extractPayload = true;
@@ -114,6 +114,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
114114

115115
private HeaderMapper<HttpHeaders> headerMapper = DefaultHttpHeaderMapper.outboundMapper();
116116

117+
@Nullable
117118
private Expression uriVariablesExpression;
118119

119120
public AbstractHttpRequestExecutingMessageHandler(Expression uriExpression) {
@@ -196,7 +197,7 @@ public void setExpectReply(boolean expectReply) {
196197
* Specify the expected response type for the REST request.
197198
* Otherwise, it is null and an empty {@link ResponseEntity} is returned from HTTP client.
198199
* To take advantage of the HttpMessageConverters
199-
* registered on this adapter, provide a different type).
200+
* registered on this adapter, provide a different type.
200201
* @param expectedResponseType The expected type.
201202
* Also see {@link #setExpectedResponseTypeExpression(Expression)}
202203
*/
@@ -322,7 +323,7 @@ protected Object handleRequestMessage(Message<?> requestMessage) {
322323

323324
@Nullable
324325
protected abstract Object exchange(Object uri, HttpMethod httpMethod, HttpEntity<?> httpRequest,
325-
Object expectedResponseType, Message<?> requestMessage, Map<String, ?> uriVariables);
326+
@Nullable Object expectedResponseType, Message<?> requestMessage, @Nullable Map<String, ?> uriVariables);
326327

327328
protected Object getReply(ResponseEntity<?> httpResponse) {
328329
HttpHeaders httpHeaders = httpResponse.getHeaders();
@@ -377,6 +378,7 @@ private HttpEntity<?> createHttpEntityFromPayload(Message<?> message, HttpMethod
377378
}
378379
HttpHeaders httpHeaders = mapHeaders(message);
379380
if (!shouldIncludeRequestBody(httpMethod)) {
381+
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
380382
return new HttpEntity<>(httpHeaders);
381383
}
382384
// otherwise, we are creating a request with a body and need to deal with the content-type header as well
@@ -514,6 +516,7 @@ private HttpMethod determineHttpMethod(Message<?> requestMessage) {
514516
}
515517
}
516518

519+
@Nullable
517520
private Object determineExpectedResponseType(Message<?> requestMessage) {
518521
return evaluateTypeFromExpression(requestMessage, this.expectedResponseTypeExpression, "expectedResponseType");
519522
}
@@ -536,9 +539,7 @@ protected Object evaluateTypeFromExpression(Message<?> requestMessage, @Nullable
536539
"evaluation resulted in a " + typeClass + ".");
537540
if (type instanceof String && StringUtils.hasText((String) type)) {
538541
try {
539-
ApplicationContext applicationContext = getApplicationContext();
540-
type = ClassUtils.forName((String) type,
541-
applicationContext == null ? null : applicationContext.getClassLoader());
542+
type = ClassUtils.forName((String) type, getApplicationContext().getClassLoader());
542543
}
543544
catch (ClassNotFoundException e) {
544545
throw new IllegalStateException("Cannot load class for name: " + type, e);

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

Lines changed: 3 additions & 3 deletions
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-2024 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.
@@ -97,7 +97,7 @@ public HttpRequestExecutingMessageHandler(Expression uriExpression) {
9797
* @param uri The URI.
9898
* @param restTemplate The rest template.
9999
*/
100-
public HttpRequestExecutingMessageHandler(String uri, RestTemplate restTemplate) {
100+
public HttpRequestExecutingMessageHandler(String uri, @Nullable RestTemplate restTemplate) {
101101
this(new LiteralExpression(uri), restTemplate);
102102
/*
103103
* We'd prefer to do this assertion first, but the compiler doesn't allow it. However,
@@ -173,7 +173,7 @@ public void setEncodingMode(DefaultUriBuilderFactory.EncodingMode encodingMode)
173173
@Override
174174
@Nullable
175175
protected Object exchange(Object uri, HttpMethod httpMethod, HttpEntity<?> httpRequest,
176-
Object expectedResponseType, Message<?> requestMessage, Map<String, ?> uriVariables) {
176+
@Nullable Object expectedResponseType, Message<?> requestMessage, @Nullable Map<String, ?> uriVariables) {
177177

178178
ResponseEntity<?> httpResponse;
179179
try {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/**
22
* Provides classes supporting outbound endpoints.
33
*/
4+
@org.springframework.lang.NonNullApi
5+
@org.springframework.lang.NonNullFields
46
package org.springframework.integration.http.outbound;

spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.core.io.buffer.DataBufferLimitException;
3333
import org.springframework.expression.Expression;
3434
import org.springframework.expression.spel.standard.SpelExpressionParser;
35+
import org.springframework.http.HttpMethod;
3536
import org.springframework.http.HttpStatus;
3637
import org.springframework.http.MediaType;
3738
import org.springframework.http.client.reactive.ClientHttpConnector;
@@ -95,6 +96,47 @@ void testReactiveReturn() {
9596
.verify(Duration.ofSeconds(10));
9697
}
9798

99+
@Test
100+
void noContentLengthHeaderForGetMethod() {
101+
ClientHttpConnector httpConnector =
102+
new HttpHandlerConnector((request, response) -> {
103+
assertThat(request.getHeaders())
104+
.doesNotContainKey(org.springframework.http.HttpHeaders.CONTENT_LENGTH);
105+
response.setStatusCode(HttpStatus.OK);
106+
return Mono.defer(response::setComplete);
107+
});
108+
109+
WebClient webClient = WebClient.builder()
110+
.clientConnector(httpConnector)
111+
.build();
112+
113+
String destinationUri = "https://www.springsource.org/spring-integration";
114+
WebFluxRequestExecutingMessageHandler reactiveHandler =
115+
new WebFluxRequestExecutingMessageHandler(destinationUri, webClient);
116+
reactiveHandler.setHttpMethod(HttpMethod.GET);
117+
118+
FluxMessageChannel ackChannel = new FluxMessageChannel();
119+
reactiveHandler.setOutputChannel(ackChannel);
120+
String testPayload = "hello, world";
121+
Message<?> testMessage =
122+
MessageBuilder.withPayload(testPayload)
123+
.setHeader(org.springframework.http.HttpHeaders.CONTENT_LENGTH, testPayload.length())
124+
.build();
125+
reactiveHandler.handleMessage(testMessage);
126+
reactiveHandler.handleMessage(testMessage);
127+
128+
StepVerifier.create(ackChannel, 2)
129+
.assertNext(m ->
130+
assertThat(m.getHeaders())
131+
.containsEntry(HttpHeaders.STATUS_CODE, HttpStatus.OK)
132+
// The reply message headers are copied from the request message
133+
.containsEntry(org.springframework.http.HttpHeaders.CONTENT_LENGTH, testPayload.length()))
134+
.assertNext(m -> assertThat(m.getHeaders()).containsEntry(HttpHeaders.STATUS_CODE, HttpStatus.OK))
135+
.expectNoEvent(Duration.ofMillis(100))
136+
.thenCancel()
137+
.verify(Duration.ofSeconds(10));
138+
}
139+
98140
@Test
99141
void testReactiveErrorOneWay() {
100142
ClientHttpConnector httpConnector =

0 commit comments

Comments
 (0)