Skip to main content

Spring Cloud 2026.0 (Paddington) Preview: Config Change Notifications and Trusted-Proxy Headers

Spring Cloud 2026.0.0-M1 adds an HTTP notifier to Config Server and fixes how Gateway handles forwarded headers from untrusted proxies. I ran both and show what changes and what to test now.

Spring Cloud 2026.0.0-M1, code-named Paddington, shipped on 24 September 2026. Its release post is short, and two lines in it matter if you run Config Server or Gateway: Config “adds an HTTP property path notifier”, and Gateway “fixes forwarded headers for untrusted proxies”. The post does not say what either means in practice, so I ran both. This post shows the behaviour, the one that could change what your services see, and a short list of what to test before the release train reaches GA.
Versions and limits. Spring Cloud 2026.0.0-M1 is a milestone: all components are 5.1.0-M1 and it is based on Spring Boot 4.2.0-M2. The current GA train is 2025.1.3 on Boot 4.1.1. I compared Gateway 5.0.3 (GA) with 5.1.0-M1, and ran Config’s spring-cloud-config-monitor 5.1.0-M1, on JDK 25 (Temurin 25.0.4.1). I tested the classes directly, not a full gateway or config server deployment. Both demos are downloadable: ForwardedDemo.java and HttpNotifierDemo.java.

The failure first: in the GA release, an untrusted proxy’s headers are dropped and nothing replaces them

When a request reaches Gateway from a caller that is not on your trusted-proxy list, Gateway must not believe the Forwarded or X-Forwarded-* headers that caller sent, because anyone can type them. The old code threw them away, which is right, but then it stopped. The upstream service received no forwarded information at all, so it could not see even the real address of the peer that called the gateway. The fix for this is the Gateway line in the release post (issue #4271 on the Spring Cloud Gateway repository: the filter “returned immediately” when the remote address was untrusted). I reproduced it. The demo builds a request from 203.0.113.9 that claims Forwarded: for=198.51.100.7;proto=https, with only 10.* trusted, and runs the two filters. On Gateway 5.0.3:
== untrusted caller 203.0.113.9: Forwarded filter
  Forwarded        = null
== untrusted caller 203.0.113.9: X-Forwarded filter
  X-Forwarded-For  = null
  X-Forwarded-Proto= null
On 5.1.0-M1:
== untrusted caller 203.0.113.9: Forwarded filter
  Forwarded        = [proto=http;for="203.0.113.9:40000"]
== untrusted caller 203.0.113.9: X-Forwarded filter
  X-Forwarded-For  = [203.0.113.9]
  X-Forwarded-Proto= [http]
The attacker’s claimed address 198.51.100.7 is gone in both versions. The difference is what is left: in 5.0.3 nothing, in 5.1.0-M1 the gateway’s own observation of who connected. (I trimmed each block to the header that the filter in question manages; each filter only touches its own header family.) The trusted caller, 10.1.2.3, behaves the same in both: its headers are kept and extended. Full output of both runs is produced by ForwardedDemo.java.
caller 203.0.113.9sends forged header Gateway 5.0.3forged removed Gateway 5.1.0-M1forged removed upstream: no for= upstream: for=203.0.113.9 (real peer)
The picture is the two console outputs side by side: the same input, the same removal of the forged value, and a different result downstream.

What it changes for you

This is a bug fix, not a tightening: the milestone makes the gateway more willing to send forwarded headers, not less. That matters in three places:
  • Upstream services that treat “no Forwarded header” as “direct request” may now see a header on traffic that came through an untrusted hop. Check IP allow-lists, rate limits and audit logs that read the client address.
  • If your gateway sits behind a load balancer that is not in the trusted-proxy setting, you used to lose the client address entirely, and now you will see the load balancer’s address. The fix for that is to configure the trusted proxies, not to rely on the old behaviour. In the demo the setting is passed to the filter constructors; in an application it is the gateway property trustedProxies (GatewayProperties has a trustedProxies field in both versions).
  • Retry in Gateway also moved from Reactor Addons Retry to Framework Core Retry in this milestone (#4290). I did not test it; if you customise retry filters, put them on your test list.
For the wider picture of pre-release testing on the same Boot line, see Spring Boot 4.2 Preview: what to test now.

Config Server can now notify applications over HTTP

Config Server’s monitor module watches a Git repository for changes and tells applications to refresh. Until now it did so through Spring Cloud Bus, which means a message broker (RabbitMQ or Kafka). The new HttpPropertyPathNotifier posts straight to each application instead. The pull request (#3279) says the old Bus notifier and the new one can coexist, and that they share an interface, PropertyPathNotifier with one method, notifyApplications(Set<String>). All three classes are in spring-cloud-config-monitor, in the package org.springframework.cloud.config.monitor. I ran the notifier against a small JDK web server standing in for the client application. A stub discovery client lists three instances of an application called orders: one with no extra information, one with the metadata refresh-endpoint=/custom/refresh, and one that is down. The changed set is orders and ghost (an application with no instances).
== 1. changed applications: orders and ghost
WARN HttpPropertyPathNotifier -- Failed to notify instance http://127.0.0.1:1
server received: POST /actuator/refresh
server received: POST /custom/refresh
requests received = 2 (3 instances registered for orders, one unreachable)
== 2. override the path per application
WARN HttpPropertyPathNotifier -- Failed to notify instance http://127.0.0.1:1
server received: POST /internal/refresh
server received: POST /internal/refresh
I removed the Java stack traces that each warning prints. Reading the output:
  • The default target is POST /actuator/refresh on every instance the discovery client reports for the changed application.
  • An instance can pick its own path with the refresh-endpoint metadata key.
  • The property spring.cloud.config.server.monitor.http.endpoints (a map from application name to path) overrides both, as the second run shows: the instance with metadata went to /internal/refresh too.
  • An unreachable instance produces a warning and the notifier carries on; no exception reaches the caller. In production that means a missed refresh shows up only in a log line.
  • The notifier needs a DiscoveryClient, so it only fits setups with service discovery.
The PR description names a switch, spring.cloud.config.server.monitor.http.enabled=true. That key is not in the configuration metadata of the 5.1.0-M1 jar, and I did not start a full Config Server to test the auto-configuration, so I cannot confirm how it is enabled. The auto-configuration classes I found are conditional on the Bus class being absent or present and on a DiscoveryClient bean. Treat the enabling step as unverified and read the milestone’s reference documentation when it is published.
Where the forwarded-header classes live (5.1.0-M1) In spring-cloud-gateway-server-webflux, package org.springframework.cloud.gateway.filter.headers: ForwardedHeadersFilter, XForwardedHeadersFilter, RemoveHopByHopHeadersFilter and the TrustedProxies interface, which has a PROPERTY constant, isTrusted(String), from(String) and filterHeaders(...). I passed the trusted-proxy regex 10\..* to the filter constructors.
Build setup used for the comparison Two Maven projects. GA: Spring Boot BOM 4.1.1 with spring-cloud-gateway-server-webflux 5.0.3. Milestone: Spring Boot BOM 4.2.0-M2 with 5.1.0-M1 and the Spring milestone repository (https://repo.spring.io/milestone). Both add spring-boot-starter-webflux and spring-test for MockServerWebExchange. The config demo uses spring-cloud-config-monitor 5.1.0-M1 and spring-boot-starter-web.

What to test now

  1. Replay a request from an address outside your trusted-proxy list through a 5.1 gateway and look at the headers your upstream receives. Compare with 5.0.x.
  2. Confirm your trusted-proxy setting really covers your load balancer. If it does not, fix that rather than depend on either behaviour.
  3. If you use Bus only to push config refreshes, try the HTTP notifier in a test environment with discovery and see whether you can retire the broker for that purpose. Watch logs for the “Failed to notify instance” warning.
  4. If you customise Gateway retry, rerun those tests, because the retry implementation changed.
  5. Resilience4J (2.4.0), Feign (13.14) and Spring Vault (4.1.0) were upgraded in the same milestone. The release post lists no breaking changes, but I tested none of these.
Should you even do this? Do not move production to Paddington yet: it is a milestone on a milestone Boot. Spend an afternoon on the first test above, though. If your upstream code uses the client address for security, the old behaviour was quietly hiding the real peer and you want to find that out on your own schedule. The HTTP notifier is only worth a look if you run Bus purely for config refresh and already use service discovery.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.