Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

`resetBeanToDefaults` transiently nulls nested `@ConfigurationProperties` objects, NPEing concurrent request threads

Aperta
#1,727 1 commento 1 reazione 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
java, spring, spring-boot
Ambito
backend, cloud

Direzione di ricerca

Inizia con resetBeanToDefaults/resetProperties e il percorso di rebind attraverso ConfigurationPropertiesBindingPostProcessor; esegui la riproduzione allegata con le versioni di spring-cloud-context elencate. Esamina come ConfigurationPropertiesBeans e ContextRefresher partecipano all’aggiornamento, quindi definisci un comportamento sicuro per le proprietà annidate durante il rebinding. Il lavoro è completato quando i test deterministici e dei lettori concorrenti passano senza NPEs e il rebinding rimane supportato.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

waiting-for-triage
Summary

Since spring-cloud-context 5.0.2 (resetBeanToDefaults, #1680), a rebind empties the live
@ConfigurationProperties singleton before repopulating it. For a bean with nested property
objects, the observable intermediate state is not merely a stale or new scalar — it is null, so a
concurrent reader does not read a wrong value, it throws NullPointerException.

This is the same window PR #1709 reported and #1721 partially addressed (writer/writer only). #1709
was closed with the observation acknowledged; this report adds the failure mode it did not cover — a
hard failure rather than a wrong value — and evidence of how often it is hit in a framework where
every service has nested properties read on request threads.

Environment
  • spring-cloud-context 5.0.2 and 5.0.3 (trains 2025.1.2, 2025.1.3); Spring Boot 4.0.8. Not present in
    4.3.x/4.2.x.
What happens
@Bean
@ConfigurationProperties(prefix = "my.service")
MyProperties myProperties() { return new MyProperties(); }

public class MyProperties {
    private Timeout timeout = null;          // populated only by binding
    public Timeout getTimeout() { return timeout; }
    // ...
}

resetProperties walks the BeanWrapper descriptors and, for every writable property, does
target.setPropertyValue(name, defaultsWrapper.getPropertyValue(name)). For timeout that is
setTimeout(null). Between resetBeanToDefaults(bean) and the binding inside
initializeBean(bean, name), every reader of myProperties.getTimeout().getX() throws NPE.

spring.cloud.refresh.never-reset-nested-types does not help: it controls recursion into a non-null
nested value, not the top level setTimeout(null).

Reproduction

rebind-reset-repro.zip is a self-contained project — Spring Boot 4.0.8 with
spring-cloud-context as the only other dependency, no Spring Cloud Config, no Consul, no
@RefreshScope. The test is also attached on its own as RebindEmptiesNestedPropertiesTest.java.txt,
and the three runs below as test-results.txt.

mvn test                                        # 5.0.3 (default) - both tests fail
mvn test -Dspring-cloud-context.version=5.0.2   # both tests fail
mvn test -Dspring-cloud-context.version=5.0.1   # both tests pass
spring-cloud-context nestedPropertyIsReadableThroughoutARebind concurrentReadersSurviveRepeatedRebinds
5.0.1 passes passes, 0 failed reads
5.0.2 fails, NPE fails, 10,942,734 of 19,659,911 reads
5.0.3 fails, NPE fails, 11,072,765 of 20,753,865 reads

The first test is deterministic rather than a race: a PriorityOrdered BeanPostProcessor reads the bean
from inside the re-bind, ahead of ConfigurationPropertiesBindingPostProcessor, which is where a request
thread lands.

NullPointerException: Cannot invoke "...MyProperties$Timeout.getConnect()"
    because the return value of "...MyProperties.getTimeout()" is null

The second is one reader thread against 200 re-binds. A tight reader loop overstates the proportion
compared with real traffic, but it shows the window is wide rather than a few instructions.

Impact and frequency

Measured in a framework used across a fleet of services, where a Consul config watch fires
ContextRefresher.refresh() on a running service:

  • In a framework integration test, 5,363 of 2,647,116 reads failed with NPE during a single
    ContextRefresher.refresh()
    - 4 reader threads resolving an HTTP connect timeout through the
    properties bean, exactly as request threads do for every outbound call.
  • In production this is a burst of failed outbound calls or 5xx per config change, not a sustained
    outage — but it is one burst per config change, per service, unattended.
  • It is aggravated by ContextRefresher.refresh() also calling scope.refreshAll(): @RefreshScope
    beans that read the properties bean are rebuilt at exactly the moment the properties they read are
    reset. An application with an async ApplicationEventMulticaster makes it near deterministic,
    because /refresh returns before the rebind has finished.
Why @RefreshScope is only a partial answer

@RefreshScope on the properties bean does work — ConfigurationPropertiesBeans skips refresh scoped
beans, so they are replaced rather than rebound — and that is the mitigation adopted here. But it has
to be applied bean by bean, by whoever owns each bean. A framework can scope its own properties beans;
it cannot scope the ones its consuming applications declare. Every application that upgrades to a
2025.1.2+ train silently acquires this exposure in its own configuration classes, with no warning at
build or start time, and the symptom surfaces as an unexplained NPE burst minutes after a config change.

Request
  1. An open issue tracking the reader visible window (#1709 was a closed PR; #750 predates the reset and
    is about a different symptom), so the state of it is discoverable.
  2. Consider making the reset opt-in — or at least opt-out per bean without losing rebinding — on 5.0.x,
    rather than only in the next major. The reset restores removed properties to defaults, which is
    valuable, but it turns a previously non-failing operation into a failing one for any bean with
    nested properties, and #1680 changed the behavior for non-proxied beans that #1662 had deliberately
    left alone for backwards compatibility.
References
  • #1680 introduced the reset (5.0.2); #1662 was the superseded variant that kept non-proxied beans safe.
  • #1709 reported the racy window with a repro; closed 2026-08-10, with the reporter noting on 2026-08-17
    that #1721 does not fix it.
  • #1721 added per-bean-name rebind locks (5.0.3) — serializes rebinders, not readers.
  • #1716 the permanent @Value nulling variant, fixed by #1720 in 5.0.3.
  • #750 open since 2020, "there never was a guarantee of thread safety refreshing @ConfigurationProperties".
Lingua principale
Java
Stelle
751
Fork
746
Merge medio
16h 35m
PR unite (30g)
23

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di spring-cloud/spring-cloud-commons

Tutte le issue di spring-cloud/spring-cloud-commons

Issue simili

Altre issue su Java

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.