Skip to content

Exclude @LoadBalanced RestClient/WebClient builders from Eureka transport - #4602

Open
arimu1 wants to merge 5 commits into
spring-cloud:mainfrom
arimu1:fix/4591-eureka-skip-loadbalanced-restclient
Open

arimu1 wants to merge 5 commits into
spring-cloud:mainfrom
arimu1:fix/4591-eureka-skip-loadbalanced-restclient

Conversation

@arimu1

@arimu1 arimu1 commented Sep 13, 2026 •

Copy link
Copy Markdown

Summary

  • Resolve Eureka registry RestClient.Builder / WebClient.Builder candidates by excluding beans annotated with @LoadBalanced, so calls to the Eureka server URL are not routed through the load balancer (fixes No instances available for localhost when only a load-balanced builder is registered).
  • Fall back to RestClient.builder() / WebClient.builder() when every registered builder is load-balanced.
  • Add context tests covering RestClient-only, plain+load-balanced, and WebClient-only setups.

Fixes gh-4591 (same root cause as gh-4524).

Test plan

  • ./mvnw -pl spring-cloud-netflix-eureka-client -Dtest=LoadBalancedEurekaHttpClientBuilderConfigurationTests test (JDK 21)
  • ./mvnw -pl spring-cloud-netflix-eureka-client -am test — module suite passes except pre-existing EurekaConfigServerBootstrapperIntegrationTests (Docker not available locally)

Eureka registry calls must not use load-balanced RestClient or WebClient
builders. Filter @LoadBalanced beans when resolving builders and fall back
to plain factory methods when needed.

Fixes spring-cloudgh-4591

Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com>
@arimu1

arimu1 commented Sep 21, 2026

Copy link
Copy Markdown
Author

Thanks for the review @ryanjbaxter — addressed all three points in eb419ef:

  1. @order / @priority — After filtering out @LoadBalanced candidates, the remaining bean names are sorted with DefaultListableBeanFactory.getOrder(beanName) (with @Order / @Priority / Ordered fallback when the factory is not a DefaultListableBeanFactory), so the highest-precedence non–load-balanced builder is chosen.

  2. Resolve once — restClientBuilder / webClientBuilder now resolve the selected builder when the supplier is created and return a supplier that always hands back that same instance (callers still .clone() per transport client as before).

  3. WebClient test — Added webClientTransportPrefersPlainWebClientBuilderOverLoadBalanced, mirroring the existing RestClient plain-vs-load-balanced test.

Tests: ./mvnw -pl spring-cloud-netflix-eureka-client -Dtest=LoadBalancedEurekaHttpClientBuilderConfigurationTests test — 4 tests, BUILD SUCCESS (Temurin 21).

Resolve the non-@LoadBalanced RestClient/WebClient builder once at
configuration time using DefaultListableBeanFactory order metadata.
Add WebClient plain-over-load-balanced configuration test.

Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com>
@arimu1
arimu1 force-pushed the fix/4591-eureka-skip-loadbalanced-restclient branch from eb419ef to 3f39a85 Compare September 21, 2026 10:41
@arimu1

arimu1 commented Sep 21, 2026

Copy link
Copy Markdown
Author

Force-pushed amended commit (removed erroneous trailer): tip is now 3f39a858c — same changes as noted above.

}

private static int orderFor(ConfigurableListableBeanFactory beanFactory, String beanName) {
if (beanFactory instanceof DefaultListableBeanFactory defaultListableBeanFactory) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause the bean to be instantiated. IMO we can just use beanFactory.findAnnotationOnBean here and if there isn't an annotation then treat it as Ordered.LOWEST_PRECEDENCE

Use findAnnotationOnBean for @Order/@priority and require
@primary or unique precedence when multiple non-@LoadBalanced
builders exist, instead of DefaultListableBeanFactory.getOrder().

Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com>
}

static Supplier<RestClient.Builder> restClientBuilder(ConfigurableListableBeanFactory beanFactory) {
RestClient.Builder builder = resolveNonLoadBalancedBuilder(beanFactory, RestClient.Builder.class,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be wrapped in a SingletonSupplier.of to prevent early initialization

}

static Supplier<WebClient.Builder> webClientBuilder(ConfigurableListableBeanFactory beanFactory) {
WebClient.Builder builder = resolveNonLoadBalancedBuilder(beanFactory, WebClient.Builder.class,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be wrapped in a SingletonSupplier.of to prevent early initialization

@kdelay kdelay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One regression to flag: getBeanNamesForType only looks at the current factory, while the ObjectProvider this replaces also resolved beans from ancestor contexts. With a plain RestClient.Builder defined only in a parent context, main hands that bean to the transport; on this branch it falls back to RestClient.builder(). I checked it with a WebApplicationContextRunner().withParent(...) test (passes on main, fails here).

Using BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type) restores it; with that change the new test and the 7 tests here all pass.

Parent contexts were invisible to getBeanNamesForType, so a plain
RestClient.Builder defined only there was skipped. Look up ancestor bean
names and read @LoadBalanced, @order, and @primary on the factory that
defines the bean. Defer that resolution until the supplier is first used.

Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com>
@arimu1

arimu1 commented Oct 11, 2026

Copy link
Copy Markdown
Author

Thanks @kdelay — f730c4050 uses BeanFactoryUtils.beanNamesForTypeIncludingAncestors, and reads @LoadBalanced / @Order / @Primary from the factory that defines the bean, so a load-balanced builder that exists only in a parent is still skipped.

Both suppliers are also SingletonSuppliers, so that lookup waits until first use.

LoadBalancedEurekaHttpClientBuilderConfigurationTests: 9 tests, 0 failures on JDK 21, including a plain builder defined only in a parent and a load-balanced builder defined only in a parent.

throw new NoUniqueBeanDefinitionException(type, primary.size(),
"more than one @Primary non-@LoadBalanced " + type.getSimpleName() + " bean: " + primary);
}
candidates.sort(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like you are calling orderFor on the candidates here and in the for loop below. That doesn't seem necessary

@arimu1

arimu1 commented Oct 27, 2026

Copy link
Copy Markdown
Author

orderFor now runs once per candidate. The tie break reads that map instead of calling it again from the sort and the loop. Tip 97a156bd4. LoadBalancedEurekaHttpClientBuilderConfigurationTests passed on JDK 17, and spring-javaformat:validate is clean.

Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com>
@arimu1
arimu1 force-pushed the fix/4591-eureka-skip-loadbalanced-restclient branch from 97a156b to a16d518 Compare October 27, 2026 00:36
@arimu1

arimu1 commented Oct 27, 2026

Copy link
Copy Markdown
Author

DCO sign-off is on tip a16d518d8 (same change as 97a156bd4).

private static <T> T resolveNonLoadBalancedBuilder(ConfigurableListableBeanFactory beanFactory, Class<T> type,
Supplier<T> fallback) {
List<String> candidates = new ArrayList<>();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, type)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the ancestor-factory handling. However I think these is a behavior change caused by this. beanNamesForTypeIncludingAncestors pools the parent and child candidates, but the previous ObjectProvider.getIfAvailable() resolved locally first and only fell back to the parent when the child had no match.

Before this PR, and with normal Spring autowiring, the child’s bean would win.

Could we add a test for this?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@LoadBalanced with RestClient` — the problem and the fix

4 participants