Description
There is a design flaw in customizing the @RestClientTest
auto-configuration MockRestServiceServerAutoConfiguration
. You cannot add your own RestClientCustomizer
bean when using @RestClientTest
in a scenario where you inject the MockRestServiceServer
bean in your test.
The POC below creates a RestClient with a content interceptor that consumes the response body twice. The test executes a request with the RestClient. The test fails because buffered content is disabled, despite it being enabled in the test via a custom RestClientCustomizer
bean.
The POC provides two different custom bean injection examples demonstrating the same effect. In particular, the second example is interesting as I found no way to workaround the problem. Hence the issue here.
I suggest to improve the Spring-Boot configuration at least for the second case. See proposal and ideas section.
Executable POC
Here you find an executable Kotlin POC demonstrating the problem (Update: Java variant)
The POC creates a RestClient with a HTTP content interceptor that consumes the response body twice. You would expect the test to pass, because it configures a RestClientCustomizer
with buffered content enabled. But it fails.
Enhancement Proposal and Ideas (for Discussion)
These proposals are based on the analysis why the injection of the custom bean does not work. Read the POC code first.
- Make
ObjectProvider<RestClientCustomizer> customizerProvider
inRestClientAutoConfiguration.restClientBuilderConfigurer(...)
order aware- i.e. implement
Ordered
- add
@Order(0)
and useAnnotationAwareOrderComparator
forObjectProvider<RestClientCustomizer> customizerProvider
- i.e. implement
- Other?
Details
- Spring Boot Version: 3.5.4
Analysis
This section is copy-paste from the provided sample code in case I ever delete the sample code from my Github account.
Overriding Bean Approach
Observation:
- Our custom bean is overridden by the default bean provided by
MockRestServiceServerAutoConfiguration
Analysis:
- Our bean loads first, then does the
MockRestServiceServerAutoConfiguration
load its bean and overrides our bean
Log file evidence:
2025-08-15T10:24:56.830+02:00 INFO 53320 --- [spring-boot-rest-client-test-customize-issue] [ Test worker] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'mockServerRestClientCustomizer' with a different definition: replacing [Root bean: class=null; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=com.example.poc.BufferingRestClientTestConfiguration; factoryMethodName=mockServerRestClientCustomizer; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in class path resource [com/example/poc/BufferingRestClientTestConfiguration.class]] with [Root bean: class=null; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerAutoConfiguration; factoryMethodName=mockServerRestClientCustomizer; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in class path resource [org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.class]]
Workaround:
- Override the
RestClientCustomizer
bean and manipulate the context loading order in the test class (seeRestClientServiceTest
). This works, but is not Spring-Boot like IMHO.
Sample Code:
@TestConfiguration
private class BufferingRestClientTestConfiguration {
// Overrides bean from MockRestServiceServerAutoConfiguration
@Bean
fun mockServerRestClientCustomizer(): MockServerRestClientCustomizer {
val customizer = MockServerRestClientCustomizer()
customizer.setBufferContent(true)
return customizer
}
}
@RestClientTest(
value = [RestClientService::class],
properties = [
"my-client.base-url=http://localhost",
// Allow mockServerRestClientCustomizer override bean. Fails otherwise.
"spring.main.allow-bean-definition-overriding=true",
],
)
@Import(
ContentInterceptor::class,
// Workaround: Load our bean after the auto-configuration. Seems not like a Spring-Boot like way (see POC README.md)
// MockRestServiceServerAutoConfiguration::class,
BufferingRestClientTestConfiguration::class,
)
internal class RestClientServiceTest @Autowired constructor(
private val mockRestServiceServer: MockRestServiceServer,
// This bean is not the one from BufferingRestClientTestConfiguration that has buffering enabled
// private val mockServerRestClientCustomizer: MockServerRestClientCustomizer,
private val unitUnderTest: RestClientService,
) {
@Test
fun testInterceptedRequest() {
mockRestServiceServer
.expect(
requestTo("http://localhost/foo"),
)
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess("hello world", MediaType.TEXT_PLAIN))
val actual = unitUnderTest.retrieveFoo()
asserter.assertEquals(null, "hello world", actual)
}
}
Additional Bean Approach
Observation:
MockRestServiceServerAutoConfiguration
consumes our bean due to the@Primary
annotationRestClientAutoConfiguration
has both beans inObjectProvider<RestClientCustomizer> customizerProvider
, but the default auto-configuration bean is the second one and thus takes precedence over our bean
Analysis:
- The
MockServerRestClientCustomizer
does not implement anything. In consequence, it becomesMAX_VALUE
and takes lowest precedence. Hence, you cannot provide a bean with a lower precedence to have it last in the list
Context for the analysis:
// File RestClientAutoConfiguration
@Bean
@ConditionalOnMissingBean
RestClientBuilderConfigurer restClientBuilderConfigurer(
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> clientHttpRequestFactoryBuilder,
ObjectProvider<ClientHttpRequestFactorySettings> clientHttpRequestFactorySettings,
ObjectProvider<RestClientCustomizer> customizerProvider) {
return new RestClientBuilderConfigurer(
clientHttpRequestFactoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect),
clientHttpRequestFactorySettings.getIfAvailable(ClientHttpRequestFactorySettings::defaults),
// Ordered stream here
customizerProvider.orderedStream().toList());
}
Workaround:
- There is no workaround known to me to make this approach work
Sample Code:
@TestConfiguration
private class BufferingRestClientAdditionalBeanTestConfiguration {
// Provide additional customizer bean
@Bean
@Primary
fun myMockServerRestClientCustomizer(): MockServerRestClientCustomizer {
val customizer = MockServerRestClientCustomizer()
customizer.setBufferContent(true)
return customizer
}
}
@RestClientTest(
value = [RestClientService::class],
properties = [
"my-client.base-url=http://localhost",
],
)
@Import(
ContentInterceptor::class,
BufferingRestClientAdditionalBeanTestConfiguration::class
)
internal class RestClientServiceMultipleCustomizerTest @Autowired constructor(
private val mockRestServiceServer: MockRestServiceServer,
// This bean is not the one from BufferingRestClientAdditionalBeanTestConfiguration that has buffering enabled
// private val mockServerRestClientCustomizer: MockServerRestClientCustomizer,
private val unitUnderTest: RestClientService,
) {
@Test
fun testInterceptedRequest() {
mockRestServiceServer
.expect(
requestTo("http://localhost/foo"),
)
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess("hello world", MediaType.TEXT_PLAIN))
val actual = unitUnderTest.retrieveFoo()
asserter.assertEquals(null, "hello world", actual)
}
}
Comment From: philwebb
@Tooa Did you use AI to generate this issue? It's quite verbose and hard to understand the actual problem you're facing.
Could you please add a description of the actual problem you're facing (without any suggested workarounds) and provide a Java sample that also shows just the problem.
Comment From: Tooa
@Tooa Did you use AI to generate this issue? It's quite verbose and hard to understand the actual problem you're facing.
No. Quite the contrary, I invested quite some time to prepare the executable code POC for you with explanations.
Could you please add a description of the actual problem you're facing (without any suggested workarounds)
Let me try to phrase the problem simpler. The goal is to enable buffered content for a @RestClientTest
. I tried two different approaches (see original issue). In both cases, the MockServerRestClientCustomizer
being taken is the one from the default auto-configuration MockRestServiceServerAutoConfiguration
with buffered content disabled.
You see this, when you execute one of the tests from the POC (e.g. RestClientServiceMultipleCustomizerTest
) in the code repository I linked. The test executes a request with a RestClient that consumes the response body twice.
The test fails, because buffered content is disabled - despite it being enabled in the test.
and provide a Java sample that also shows just the problem.
What is wrong with the Kotlin example in the repository I linked in the issue? From my point of view, it clearly shows the problem. You would expect the test to pass, but it fails because the default bean with disabled buffered content is being taken. Have you had a look at the repository?
Comment From: snicoll
No. Quite the contrary, I invested quite some time to prepare the executable code POC for you with explanations.
Thanks. Unfortunately, it's far more easier for us to get to the point with a sample that showcases the actual problem rather than reading through your own analysis. We will have to go through the code ourselves anyways to make up our own mind and additional details are certainly welcome if that doesn't proved fruitful.
What is wrong with the Kotlin example in the repository I linked in the issue?
I think what Phil meant is that a minimal sample should be in Java ideally. Kotlin or additional libraries (like Lombok) has proven to be in the way of debugging and also can induce additional side effects. The goal here is to optimize our time as much as possible.
Comment From: Tooa
Thanks. Unfortunately, it's far more easier for us to get to the point with a sample that showcases the actual problem rather than reading through your own analysis
I see. Then ignore everything written in the README.md
of the sample code and just execute one of the tests and see it fail. You would expect it to pass. That is the problem.
I think what Phil meant is that a minimal sample should be in Java ideally. Kotlin or additional libraries (like Lombok) has proven to be in the way of debugging and also can induce additional side effects. The goal here is to optimize our time as much as possible.
I am sure this is not a Kotlin specific issue. But I understand your point. I will provide a Java sample.
Edit: I also re-worked the original issue. Moving the most relevant information up-front and adding details that where only present in the sample code. Hope this helps!
Comment From: Tooa
Here is the Java sample code. This time no fluff. The tests in example1
and example2
show the same problem with two different ways to configure a custom bean for enabling buffered content. You would expect the tests to pass in both cases. But they fail.
Comment From: philwebb
No. Quite the contrary, I invested quite some time to prepare the executable code POC for you with explanations.
Sorry, no offense was meant by comment. We've seen quite an uptick in AI generated pull-requests and issues and I wanted to check that wasn't the case here.
Comment From: philwebb
I think as things stand, things are a little clunky and we should probably make the beans in MockRestServiceServerAutoConfiguration
@ConditionalOnMissingBean
.
In the meantime, you can do the following:
@RestClientTest(value = RestClientService.class, properties = { "my-client.base-url=http://localhost", })
@AutoConfigureMockRestServiceServer(enabled = false)
@Import({ContentInterceptor.class,BufferingRestClientAdditionalBeanTestConfiguration.class})
public class RestClientServiceTest {
private final MockRestServiceServer mockRestServiceServer;
private final RestClientService unitUnderTest;
@Autowired
public RestClientServiceTest(MockServerRestClientCustomizer restClientCustomizer, RestClientService unitUnderTest) {
mockRestServiceServer = restClientCustomizer.getServer();
this.unitUnderTest = unitUnderTest;
}
@Test
void testInterceptRequest() {
mockRestServiceServer.expect(requestTo("http://localhost/foo")).andExpect(method(HttpMethod.GET))
.andRespond(withSuccess("hello world", MediaType.TEXT_PLAIN));
var actual = unitUnderTest.retrieveFoo();
assertEquals("hello world", actual);
}
}
@TestConfiguration
public class BufferingRestClientAdditionalBeanTestConfiguration {
@Bean
public MockServerRestClientCustomizer myMockServerRestClientCustomizer() {
var customizer = new MockServerRestClientCustomizer();
customizer.setBufferContent(true);
return customizer;
}
}
This will disable our auto-configuration, allowing you to create the customizer you want.
Comment From: philwebb
Flagging for team attention to see if we consider this a bug of omission or an enhancement.