diff --git a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java index 58d8cec61ae..fa65cd57d02 100644 --- a/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java +++ b/spring-web/src/main/java/org/springframework/http/client/support/HttpAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -20,12 +20,15 @@ import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.function.BiPredicate; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.HttpLogging; import org.springframework.http.HttpMethod; +import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestInitializer; @@ -57,6 +60,8 @@ public abstract class HttpAccessor { private final List clientHttpRequestInitializers = new ArrayList<>(); + private @Nullable BiPredicate bufferingPredicate; + /** * Set the request factory that this accessor uses for obtaining client request handles. @@ -78,10 +83,11 @@ public abstract class HttpAccessor { * Return the request factory that this accessor uses for obtaining client request handles. */ public ClientHttpRequestFactory getRequestFactory() { - return this.requestFactory; + return (this.bufferingPredicate != null ? + new BufferingClientHttpRequestFactory(this.requestFactory, this.bufferingPredicate) : + this.requestFactory); } - /** * Set the request initializers that this accessor should use. *

The initializers will get immediately sorted according to their @@ -111,6 +117,25 @@ public abstract class HttpAccessor { return this.clientHttpRequestInitializers; } + /** + * Enable buffering of request and response, aggregating all content before + * it is sent, and making it possible to read the response body repeatedly. + * @param predicate to determine whether to buffer for the given request + * @since 7.0 + */ + public void setBufferingPredicate(@Nullable BiPredicate predicate) { + this.bufferingPredicate = predicate; + } + + /** + * Return the {@link #setBufferingPredicate(BiPredicate) configured} predicate + * to determine whether to buffer request and response content. + * @since 7.0 + */ + public @Nullable BiPredicate getBufferingPredicate() { + return this.bufferingPredicate; + } + /** * Create a new {@link ClientHttpRequest} via this template's {@link ClientHttpRequestFactory}. * @param url the URL to connect to diff --git a/spring-web/src/main/java/org/springframework/http/client/support/InterceptingHttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingHttpAccessor.java index d1971fda661..4b72665e219 100644 --- a/spring-web/src/main/java/org/springframework/http/client/support/InterceptingHttpAccessor.java +++ b/spring-web/src/main/java/org/springframework/http/client/support/InterceptingHttpAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -99,7 +99,8 @@ public abstract class InterceptingHttpAccessor extends HttpAccessor { if (!CollectionUtils.isEmpty(interceptors)) { ClientHttpRequestFactory factory = this.interceptingRequestFactory; if (factory == null) { - factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors); + factory = new InterceptingClientHttpRequestFactory( + super.getRequestFactory(), interceptors, getBufferingPredicate()); this.interceptingRequestFactory = factory; } return factory; diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java index 1ae72aa429f..597a7b2074a 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java @@ -185,6 +185,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder { if (!CollectionUtils.isEmpty(restTemplate.getInterceptors())) { this.interceptors = new ArrayList<>(restTemplate.getInterceptors()); } + this.bufferingPredicate = restTemplate.getBufferingPredicate(); if (!CollectionUtils.isEmpty(restTemplate.getClientHttpRequestInitializers())) { this.initializers = new ArrayList<>(restTemplate.getClientHttpRequestInitializers()); } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index cf7f6fadd44..b9cbab2a003 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -52,10 +52,13 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.SmartHttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.util.FileCopyUtils; import org.springframework.web.util.DefaultUriBuilderFactory; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -765,6 +768,46 @@ class RestTemplateTests { verify(response).close(); } + @Test + void requestInterceptorWithBuffering() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue(new MockResponse().setResponseCode(200).setBody("Hello Spring!")); + server.start(); + template.setRequestFactory(new SimpleClientHttpRequestFactory()); + template.setInterceptors(List.of((request, body, execution) -> { + ClientHttpResponse response = execution.execute(request, body); + byte[] result = FileCopyUtils.copyToByteArray(response.getBody()); + assertThat(result).isEqualTo("Hello Spring!".getBytes(UTF_8)); + return response; + })); + template.setBufferingPredicate((uri, httpMethod) -> true); + template.setMessageConverters(List.of(new StringHttpMessageConverter())); + String result = template.getForObject(server.url("/").uri(), String.class); + assertThat(server.getRequestCount()).isEqualTo(1); + assertThat(result).isEqualTo("Hello Spring!"); + } + } + + @Test + void buffering() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue(new MockResponse().setResponseCode(200).setBody("Hello Spring!")); + server.start(); + template.setRequestFactory(new SimpleClientHttpRequestFactory()); + template.setBufferingPredicate((uri, httpMethod) -> true); + template.setMessageConverters(List.of(new StringHttpMessageConverter())); + String result = template.execute(server.url("/").uri(), HttpMethod.GET, req -> {}, response -> { + byte[] bytes = FileCopyUtils.copyToByteArray(response.getBody()); + assertThat(bytes).isEqualTo("Hello Spring!".getBytes(UTF_8)); + bytes = FileCopyUtils.copyToByteArray(response.getBody()); + assertThat(bytes).isEqualTo("Hello Spring!".getBytes(UTF_8)); + return new String(bytes, UTF_8); + }); + assertThat(server.getRequestCount()).isEqualTo(1); + assertThat(result).isEqualTo("Hello Spring!"); + } + } + @Test void clientHttpRequestInitializerAndRequestInterceptorAreBothApplied() throws Exception { ClientHttpRequestInitializer initializer = request ->