spring-cloud / spring-cloud/spring-cloud-netflix
@LoadBalanced` with `RestClient` — the problem and the fix
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 5k
- Forks
- 2.5k
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 10
Description
@LoadBalanced with RestClient — the problem and the fix
Spring Boot 4.1.1 · Spring Cloud 2025.1.2 · Eureka
The problem
You add a @LoadBalanced RestClient.Builder, expecting it to work the way @LoadBalanced RestTemplate
always did. Instead your application never registers with Eureka, and the log repeats:
java.lang.IllegalStateException: No instances available for localhost
WARN ... DiscoveryClient : registration failed Cannot execute request on any known server
ERROR ... DiscoveryClient : was unable to send heartbeat!
The application still starts, which is what makes it confusing — it just silently never appears in
the registry.
Why it happens
The Eureka client needs an HTTP client for its own calls to the registry, and it looks for a
RestClient.Builder in your application context:
// DiscoveryClientOptionalArgsConfiguration.RestClientConfiguration
ObjectProvider<RestClient.Builder> restClientBuilderProvider
...
() -> restClientBuilderProvider.getIfAvailable(RestClient::builder);
If the only RestClient.Builder bean you have is the @LoadBalanced one, Eureka finds and uses it.
Its calls to http://localhost:8761/eureka then go through the load balancer, which tries to resolve
localhost as a service id and fails.
Two details make this easy to hit:
LoadBalancerRestClientBuilderBeanPostProcessorhas already added the load balancer interceptor to
that builder instance, so anything resolving the bean inherits the interceptor.- Declaring any
RestClient.Builderbean suppresses Boot's auto-configured one
(RestClientAutoConfiguration#restClientBuilderis@ConditionalOnMissingBean), which removes the
plain fallback candidate and leaves the load-balanced builder as the only match.
The fix
Keep a second, plain @Primary builder so the Eureka transport has an ordinary candidate to find,
and ask for the load-balanced one explicitly by qualifier:
@Configuration
public class RestClientConfig {
/** Plain and @Primary — this is the one the Eureka transport picks up. */
@Bean
@Primary
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RestClient.Builder restClientBuilder(RestClientBuilderConfigurer configurer) {
return configurer.configure(RestClient.builder());
}
/** Load-balanced — gets the interceptor from the bean post processor. */
@Bean
@LoadBalanced
public RestClient.Builder loadBalancedRestClientBuilder(RestClientBuilderConfigurer configurer) {
return configurer.configure(RestClient.builder());
}
/** Note the @LoadBalanced on the parameter — see rule 2 below. */
@Bean
public RestClient serviceARestClient(@LoadBalanced RestClient.Builder builder) {
return builder.baseUrl("http://SERVICE-A").build();
}
}
With this in place, registration succeeds and requests round-robin correctly across instances.
Two rules to remember
1. Don't delete the plain builder. It looks like dead code. It is the entire fix — it exists so
the Eureka transport has something non-load-balanced to resolve.
2. @LoadBalanced must also be on the injection point.
// WRONG — compiles, runs, quietly does no load balancing
public RestClient serviceARestClient(RestClient.Builder loadBalancedRestClientBuilder) { ... }
// RIGHT
public RestClient serviceARestClient(@LoadBalanced RestClient.Builder builder) { ... }
Naming the parameter after the bean does not select it. DefaultListableBeanFactory#determineAutowireCandidate
checks for a @Primary candidate first and only falls back to parameter-name matching afterwards,
so the plain builder wins. @LoadBalanced is itself a @Qualifier, which is why putting it on the
parameter works.
Why RestTemplate never had this problem
@LoadBalanced RestTemplate |
@LoadBalanced RestClient.Builder |
|
|---|---|---|
| Load balancing works | yes | yes |
| Eureka registration affected | no | yes — needs the extra plain builder |
| Why | Eureka's transport doesn't consume RestTemplate beans |
Eureka's transport resolves RestClient.Builder by type |
WebClient.Builder is resolved the same way as RestClient.Builder, but only when
eureka.client.webclient.enabled=true, so a default setup never reaches that path.
Dependency note
RestClientBuilderConfigurer is org.springframework.boot.restclient.autoconfigure.RestClientBuilderConfigurer
and comes from the spring-boot-restclient module. On Boot 4, spring-boot-starter-webmvc does not
bring it in, so add:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
Without it there is no auto-configured RestClient.Builder at all, and the snippet above won't compile.
Upstream
Reported in spring-cloud-netflix#4524
and spring-cloud-commons#1633.
Both open at the time of writing — check them before assuming this workaround is still needed.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the Eureka registration failure with the RestClient.Builder setup described in the issue. Read DiscoveryClientOptionalArgsConfiguration.RestClientConfiguration, LoadBalancerRestClientBuilderBeanPostProcessor, and RestClientAutoConfiguration#restClientBuilder; done means Eureka registration succeeds while service requests remain load-balanced.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring, spring-boot
- Domain
- backend, cloud, distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100