From 31090da79abfad674bf708e720e28ee35a73fd3e Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 18 Sep 2024 13:49:45 -0400 Subject: [PATCH] GH-9489: Remove `Content-Length` HTTP before sending GET request Fixes: #9489 Issue link: https://github.com/spring-projects/spring-integration/issues/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 891dca71796631c15c83596eb3e6d33194b6c6c8) --- ...actHttpRequestExecutingMessageHandler.java | 15 +++---- .../HttpRequestExecutingMessageHandler.java | 6 +-- .../http/outbound/package-info.java | 2 + ...uxRequestExecutingMessageHandlerTests.java | 42 +++++++++++++++++++ 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java index e279004c21..db98e61a37 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java @@ -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. @@ -32,7 +32,6 @@ import java.util.concurrent.locks.ReentrantLock; 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; @@ -100,6 +99,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac private boolean expectReply = true; + @Nullable private Expression expectedResponseTypeExpression; private boolean extractPayload = true; @@ -114,6 +114,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac private HeaderMapper headerMapper = DefaultHttpHeaderMapper.outboundMapper(); + @Nullable private Expression uriVariablesExpression; public AbstractHttpRequestExecutingMessageHandler(Expression uriExpression) { @@ -196,7 +197,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. * 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)} */ @@ -322,7 +323,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac @Nullable protected abstract Object exchange(Object uri, HttpMethod httpMethod, HttpEntity httpRequest, - Object expectedResponseType, Message requestMessage, Map uriVariables); + @Nullable Object expectedResponseType, Message requestMessage, @Nullable Map uriVariables); protected Object getReply(ResponseEntity httpResponse) { HttpHeaders httpHeaders = httpResponse.getHeaders(); @@ -377,6 +378,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac } 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 @@ -514,6 +516,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac } } + @Nullable private Object determineExpectedResponseType(Message requestMessage) { return evaluateTypeFromExpression(requestMessage, this.expectedResponseTypeExpression, "expectedResponseType"); } @@ -536,9 +539,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac "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); diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java index e1fbe5c087..b9ad0e598e 100755 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java @@ -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. @@ -97,7 +97,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractHttpRequestExecu * @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, @@ -173,7 +173,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractHttpRequestExecu @Override @Nullable protected Object exchange(Object uri, HttpMethod httpMethod, HttpEntity httpRequest, - Object expectedResponseType, Message requestMessage, Map uriVariables) { + @Nullable Object expectedResponseType, Message requestMessage, @Nullable Map uriVariables) { ResponseEntity httpResponse; try { diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/package-info.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/package-info.java index c829cdd5c7..63a84c612a 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/package-info.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/package-info.java @@ -1,4 +1,6 @@ /** * Provides classes supporting outbound endpoints. */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields package org.springframework.integration.http.outbound; diff --git a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java index 29893db779..1cfaba4502 100644 --- a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java +++ b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java @@ -32,6 +32,7 @@ import org.springframework.core.io.buffer.DataBufferFactory; 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; @@ -95,6 +96,47 @@ class WebFluxRequestExecutingMessageHandlerTests { .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 =