Skip to content

Commit

Permalink
GH-9489: Remove Content-Length HTTP before sending GET request
Browse files Browse the repository at this point in the history
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`

**Auto-cherry-pick to `6.3.x` & `6.2.x`**
  • Loading branch information
artembilan committed Sep 18, 2024
1 parent a6f9e78 commit 891dca7
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2023 the original author or authors.
* Copyright 2017-2024 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.
Expand Down Expand Up @@ -32,7 +32,6 @@
import javax.xml.transform.Source;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
Expand Down Expand Up @@ -100,6 +99,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac

private boolean expectReply = true;

@Nullable
private Expression expectedResponseTypeExpression;

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

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

@Nullable
private Expression uriVariablesExpression;

public AbstractHttpRequestExecutingMessageHandler(Expression uriExpression) {
Expand Down Expand Up @@ -196,7 +197,7 @@ public void setExpectReply(boolean expectReply) {
* Specify the expected response type for the REST request.
* 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).
* registered on this adapter, provide a different type.
* @param expectedResponseType The expected type.
* Also see {@link #setExpectedResponseTypeExpression(Expression)}
*/
Expand Down Expand Up @@ -322,7 +323,7 @@ protected Object handleRequestMessage(Message<?> requestMessage) {

@Nullable
protected abstract Object exchange(Object uri, HttpMethod httpMethod, HttpEntity<?> httpRequest,
Object expectedResponseType, Message<?> requestMessage, Map<String, ?> uriVariables);
@Nullable Object expectedResponseType, Message<?> requestMessage, @Nullable Map<String, ?> uriVariables);

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

@Nullable
private Object determineExpectedResponseType(Message<?> requestMessage) {
return evaluateTypeFromExpression(requestMessage, this.expectedResponseTypeExpression, "expectedResponseType");
}
Expand All @@ -536,9 +539,7 @@ protected Object evaluateTypeFromExpression(Message<?> requestMessage, @Nullable
"evaluation resulted in a " + typeClass + ".");
if (type instanceof String && StringUtils.hasText((String) type)) {
try {
ApplicationContext applicationContext = getApplicationContext();
type = ClassUtils.forName((String) type,
applicationContext == null ? null : applicationContext.getClassLoader());
type = ClassUtils.forName((String) type, getApplicationContext().getClassLoader());
}
catch (ClassNotFoundException e) {
throw new IllegalStateException("Cannot load class for name: " + type, e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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.
Expand Down Expand Up @@ -97,7 +97,7 @@ public HttpRequestExecutingMessageHandler(Expression uriExpression) {
* @param uri The URI.
* @param restTemplate The rest template.
*/
public HttpRequestExecutingMessageHandler(String uri, RestTemplate restTemplate) {
public HttpRequestExecutingMessageHandler(String uri, @Nullable RestTemplate restTemplate) {
this(new LiteralExpression(uri), restTemplate);
/*
* We'd prefer to do this assertion first, but the compiler doesn't allow it. However,
Expand Down Expand Up @@ -173,7 +173,7 @@ public void setEncodingMode(DefaultUriBuilderFactory.EncodingMode encodingMode)
@Override
@Nullable
protected Object exchange(Object uri, HttpMethod httpMethod, HttpEntity<?> httpRequest,
Object expectedResponseType, Message<?> requestMessage, Map<String, ?> uriVariables) {
@Nullable Object expectedResponseType, Message<?> requestMessage, @Nullable Map<String, ?> uriVariables) {

ResponseEntity<?> httpResponse;
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/**
* Provides classes supporting outbound endpoints.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.integration.http.outbound;
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
Expand Down Expand Up @@ -95,6 +96,47 @@ void testReactiveReturn() {
.verify(Duration.ofSeconds(10));
}

@Test
void noContentLengthHeaderForGetMethod() {
ClientHttpConnector httpConnector =
new HttpHandlerConnector((request, response) -> {
assertThat(request.getHeaders())
.doesNotContainKey(org.springframework.http.HttpHeaders.CONTENT_LENGTH);
response.setStatusCode(HttpStatus.OK);
return Mono.defer(response::setComplete);
});

WebClient webClient = WebClient.builder()
.clientConnector(httpConnector)
.build();

String destinationUri = "https://www.springsource.org/spring-integration";
WebFluxRequestExecutingMessageHandler reactiveHandler =
new WebFluxRequestExecutingMessageHandler(destinationUri, webClient);
reactiveHandler.setHttpMethod(HttpMethod.GET);

FluxMessageChannel ackChannel = new FluxMessageChannel();
reactiveHandler.setOutputChannel(ackChannel);
String testPayload = "hello, world";
Message<?> testMessage =
MessageBuilder.withPayload(testPayload)
.setHeader(org.springframework.http.HttpHeaders.CONTENT_LENGTH, testPayload.length())
.build();
reactiveHandler.handleMessage(testMessage);
reactiveHandler.handleMessage(testMessage);

StepVerifier.create(ackChannel, 2)
.assertNext(m ->
assertThat(m.getHeaders())
.containsEntry(HttpHeaders.STATUS_CODE, HttpStatus.OK)
// The reply message headers are copied from the request message
.containsEntry(org.springframework.http.HttpHeaders.CONTENT_LENGTH, testPayload.length()))
.assertNext(m -> assertThat(m.getHeaders()).containsEntry(HttpHeaders.STATUS_CODE, HttpStatus.OK))
.expectNoEvent(Duration.ofMillis(100))
.thenCancel()
.verify(Duration.ofSeconds(10));
}

@Test
void testReactiveErrorOneWay() {
ClientHttpConnector httpConnector =
Expand Down

0 comments on commit 891dca7

Please sign in to comment.