spring-cloud / spring-cloud/spring-cloud-netflix

@LoadBalanced` with `RestClient` — the problem and the fix

Open
#4,591 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

waiting-for-triage
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:

  • LoadBalancerRestClientBuilderBeanPostProcessor has already added the load balancer interceptor to
    that builder instance, so anything resolving the bean inherits the interceptor.
  • Declaring any RestClient.Builder bean suppresses Boot's auto-configured one
    (RestClientAutoConfiguration#restClientBuilder is @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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.