Skip to content

Allow repeatable writes in StreamingHttpOutputMessage #31449

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
cachescrubber opened this issue Oct 17, 2023 · 12 comments
Closed

Allow repeatable writes in StreamingHttpOutputMessage #31449

cachescrubber opened this issue Oct 17, 2023 · 12 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Milestone

Comments

@cachescrubber
Copy link

cachescrubber commented Oct 17, 2023

BufferingClientHttpRequestFactory creates requests which are not repeatable

A RestTemplate obtained form the following snippet will lose the default request configuration applied to the HttpComponentsClientHttpRequestFactory, in this case the setRedirectsEnabled(true) is not honored.

  • Affected Version: spring-boot 3.2.0-M3
  • Works with stable: spring-boot 3.1.4
  • Affected http client library is Apache HttpComponents 5 (HttpComponentsClientHttpRequestFactory).

I prepared a reproducible example here. Just run com.example.httpclient5demo.Httpclient5DemoApplicationTests

		RestTemplate restTemplate = restTemplateBuilder
				.requestFactory(() -> {
					RequestConfig.Builder requestConfig = RequestConfig.custom()
							.setRedirectsEnabled(true);
					HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
							.setDefaultRequestConfig(requestConfig.build());
					HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClientBuilder.build());
					//return requestFactory;
					return new BufferingClientHttpRequestFactory(requestFactory);
				})
				.rootUri("https://httpbin.org")
				.build();
		String response = restTemplate.getForObject("/redirect-to?url=https://httpbin.org/base64/SFRUUEJJTiBpcyBhd2Vzb21l", String.class);
		assertThat(response).isEqualTo("HTTPBIN is awesome");

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Oct 17, 2023
@wilkinsona
Copy link
Member

Thanks for the report and sample project. This doesn't appear to be a Spring Boot problem. The following tests illustrate the behavior without using Spring Boot:

package com.example.httpclient5demo;

import static org.assertj.core.api.Assertions.assertThat;

import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

class Httpclient5DemoApplicationTests {
	
	@Test
	void redirectsCanBeEnabled() {
		RestTemplate restTemplate = new RestTemplate();
		RequestConfig.Builder requestConfig = RequestConfig.custom()
				.setRedirectsEnabled(true);
		HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
				.setDefaultRequestConfig(requestConfig.build());
		HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClientBuilder.build());
		restTemplate.setRequestFactory(requestFactory);
		String response = restTemplate.getForObject("https://httpbin.org/redirect-to?url=https://httpbin.org/base64/SFRUUEJJTiBpcyBhd2Vzb21l", String.class);
		assertThat(response).isEqualTo("HTTPBIN is awesome");
	}

	@Test
	void redirectsCanBeEnabledWhenBuffering() {
		RestTemplate restTemplate = new RestTemplate();
		RequestConfig.Builder requestConfig = RequestConfig.custom()
				.setRedirectsEnabled(true);
		HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
				.setDefaultRequestConfig(requestConfig.build());
		HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClientBuilder.build());
		restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(requestFactory));
		String response = restTemplate.getForObject("https://httpbin.org/redirect-to?url=https://httpbin.org/base64/SFRUUEJJTiBpcyBhd2Vzb21l", String.class);
		assertThat(response).isEqualTo("HTTPBIN is awesome");
	}

}

redirectsCanBeEnabled passes but redirectsCanBeEnabledWhenBuffering fails. We'll transfer this to the Framework team so that they can take a look.

@snicoll snicoll transferred this issue from spring-projects/spring-boot Oct 17, 2023
@cachescrubber
Copy link
Author

Thank you @wilkinsona for analyzing the issue. I updated the example with your test cases.

@cachescrubber
Copy link
Author

I spotted another issue. Basic Auth (401) and Proxy Auth (407) Challenges are not working when buffering is enabled.

	@Test
	void basicAuthCanBeEnabledWhenBuffering() {
		RestTemplate restTemplate = new RestTemplate();
		BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();

		credentialsProvider.setCredentials(new AuthScope("httpbin.org", 443),
				new UsernamePasswordCredentials("demo", "secret".toCharArray()));

		HttpClientBuilder httpClientBuilder = HttpClientBuilder.create()
				.setDefaultCredentialsProvider(credentialsProvider);

		HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClientBuilder.build());
		restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(requestFactory));
		ResponseEntity<String> forEntity = restTemplate.getForEntity("https://httpbin.org//basic-auth/demo/secret", String.class);
		assertThat(forEntity.getStatusCode().is2xxSuccessful()).isTrue();
	}

The example has been updated with a test case.

@cachescrubber
Copy link
Author

The issue is not a configuration issue, as I assumed initialy. I'll change the issue title accordingly.

It is caused by changes introduced in #30557 . BufferingClientHttpRequestWrapper is creating an empty body, which is passed to HttpComponents as org.springframework.http.client.HttpComponentsClientHttpRequest.BodyEntity which in turn implements org.apache.hc.core5.http.HttpEntity#isRepeatable to return false unconditionally.

HttpEntity#isRepeatable returning false disables all kind of functionality in the library where requests are repeated - redirects (3xx), authentication challenges (401,407) but also internal retries.

A quick fix would be to not pass in empty HttpComponentsClientHttpRequest.BodyEntity for requests where no body is expected (GET, HEAD, ...).

But I think the changes in #30557 are problematic in this regard. As it is implemented right now, even requests using the BufferingClientHttpRequestFactory are converted into a streaming body, even though they are in-memory anyway.

@cachescrubber cachescrubber changed the title RestTemplateBuilder with a BufferingClientHttpRequestFactory looses the wrapped RequestFactory BufferingClientHttpRequestFactory creates requests which are not repeatable Oct 23, 2023
@poutsma poutsma self-assigned this Oct 23, 2023
@poutsma poutsma added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Oct 23, 2023
@poutsma poutsma added this to the 6.1.0-RC2 milestone Oct 23, 2023
@poutsma poutsma changed the title BufferingClientHttpRequestFactory creates requests which are not repeatable Allow repeatable writes in StreamingHttpOutputMessage Oct 24, 2023
@poutsma poutsma added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Oct 24, 2023
@cachescrubber
Copy link
Author

Thanks @poutsma , my HttpComponents based test are good again. From reading the code I think at least OkHttp3ClientHttpRequest.BodyRequestBody#isOneShot need to be updated as well. Not sure for the other implementations.

@poutsma
Copy link
Contributor

poutsma commented Oct 24, 2023

Thanks for spotting that, it should now be fixed.

poutsma added a commit that referenced this issue Nov 2, 2023
This commit ensures that the StreamingHttpOutputMessage.Body.repeatable
flag is set in message converters for bodies that can be written
repeatedly.

Closes gh-31516
See gh-31449
@poutsma
Copy link
Contributor

poutsma commented Nov 2, 2023

@cachescrubber I made some further related changes in 6dd93d4, which should enable repeatable writes for many types of request bodies, meaning that you typically don't need to wrap your request factory in a BufferingClientHttpRequestFactory any more. Please try a 6.1.0 Spring Framework snapshot and see if it works for you.

@cachescrubber
Copy link
Author

Thanks for the heads-up. I use BufferingClientHttpRequestFactory to implement request/response logging in a ClientHttpRequestInterceptor. IIRC we needed the buffering in order to use response#getBody multiple times.

@karlovskiy
Copy link

Hi, I found this issue today during my investigation of redirects which stopped working after spring update.
I found that in this line we have
streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
but if body was set like this with lambda than repeatable will be false.
Is it intended behavior ?
I thought that if we have byte array in InterceptingClientHttpRequest maybe body there also should be repeatable ?

PS: Currently, I resolved my redirect issue with wrapping HttpComponentsClientHttpRequestFactory with BufferingClientHttpRequestFactory.

@poutsma
Copy link
Contributor

poutsma commented Feb 14, 2024

@karlovskiy well spotted, this should be fixed in 6.1.4.

@NicklasWallgren
Copy link

NicklasWallgren commented May 7, 2025

@poutsma

Are there any plans to add retryability support for multi-part requests inFormHttpMessageConverter#writeMultipart? The StreamingHttpOutputMessage#body lambda which is generated at

always return false

@poutsma
Copy link
Contributor

poutsma commented May 7, 2025

Hi @NicklasWallgren,

I am no longer employed by Broadcom and no longer have commit rights on the Spring Framework, so I am not involved in its maintenance.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

6 participants