Skip to content

fix: preserve Nacos config file extension across refresh#4312

Open
daguimu wants to merge 2 commits into
alibaba:2025.1.xfrom
daguimu:fix/nacos-config-refresh-file-extension-issue4296
Open

fix: preserve Nacos config file extension across refresh#4312
daguimu wants to merge 2 commits into
alibaba:2025.1.xfrom
daguimu:fix/nacos-config-refresh-file-extension-issue4296

Conversation

@daguimu

@daguimu daguimu commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Problem

When a Nacos config change is pushed to the client, NacosPropertySourceRefreshListener#handle(NacosConfigRefreshEvent) rebuilds the affected NacosPropertySource by calling:

nacosPropertySourceBuilder.build(dataId, group, "properties", prevpropertySource.isRefreshable());

The third argument — the file extension — is hard-coded as "properties". NacosDataParserHandler#parseNacosData then selects PropertiesPropertySourceLoader regardless of the user's actual format (yaml, yml, json, xml). So after the first refresh, a yaml config like

server:
  port: 8080

is re-parsed as a properties file and produces a single bogus key server:\n port with value 8080, replacing the correctly-parsed yaml source that was installed at startup.

Root Cause

NacosPropertySource never remembers the extension it was created with. At refresh time the listener therefore has no way to recover the original format and defaults to "properties". This affects both the main spring.cloud.nacos.config.file-extension as well as extension-configs / shared-configs that may declare their own suffix per entry.

Fix

  • Add an optional fileExtension field to NacosPropertySource with a new public constructor overload and a getFileExtension() accessor. The existing constructors stay for backward compatibility.
  • NacosPropertySourceBuilder#build already receives the file extension from its callers, so it now passes it into the new constructor.
  • NacosConfigDataLoader#doLoad propagates NacosItemConfig#getSuffix(), which is already the per-entry suffix, so extension-configs/shared-configs with non-default formats are also preserved.
  • NacosPropertySourceRefreshListener#handle reads the preserved extension from the previous NacosPropertySource, falls back to NacosConfigProperties#getFileExtension() (useful when the source was built via the legacy constructor from third-party code), and finally keeps the historical "properties" default so nothing regresses.

Tests Added

Change Point Test
Legacy constructor leaves fileExtension unset NacosPropertySourceTest#fileExtensionIsNullWhenUsingLegacyConstructor
New constructor preserves the provided extension NacosPropertySourceTest#fileExtensionIsPreservedWhenProvided, otherPropertiesAreNotAffectedByNewConstructor
New constructor accepts null without side effects NacosPropertySourceTest#fileExtensionCanBeNullExplicitly
Builder#build propagates the yaml extension into the resulting NacosPropertySource NacosPropertySourceBuilderTest#builtPropertySourcePreservesFileExtension
Builder#build propagates the properties extension NacosPropertySourceBuilderTest#builtPropertySourcePreservesPropertiesExtension

Impact

  • Yaml / yml / json / xml Nacos configs retain their correct parser across pushes — the refresh path now mirrors the initial-load path.
  • Properties users see no behavioral change: their extension is preserved explicitly instead of implicitly re-defaulting.
  • Third-party code that still calls the legacy NacosPropertySource constructor keeps working; the refresh listener's two-step fallback covers that case.
  • Scope is limited to spring-alibaba-nacos-config; no public contract on NacosPropertySource was removed.

Addresses #4296

daguimu added 2 commits April 23, 2026 23:59
When a Nacos config update is received, NacosPropertySourceRefreshListener
rebuilt the affected NacosPropertySource with a hard-coded "properties"
file extension, so yaml/yml/json/xml configs were re-parsed as properties
and silently produced broken key/value pairs after each push.

- Carry the original file extension on NacosPropertySource via a new
  constructor overload and expose it through a getter.
- NacosPropertySourceBuilder and NacosConfigDataLoader populate the
  extension when they create the source (using the dataId suffix /
  builder argument that was already in scope).
- The refresh listener now reads the preserved extension from the
  previous NacosPropertySource and falls back to
  NacosConfigProperties.getFileExtension(), then to "properties", so
  behavior for historical sources built via the legacy constructor
  stays unchanged.

Addresses alibaba#4296
Address review feedback on the file-extension-preserving refresh fix:

- Make NacosPropertySource#fileExtension final and route every constructor
  through a single all-args constructor so the field is safely published
  to the ApplicationEvent dispatch thread that reads it on refresh.
- Drop the intermediate fallback to NacosConfigProperties#getFileExtension
  in the refresh listener: it could re-introduce the same bug for
  shared-configs / extension-configs that declare a per-entry suffix but
  run under a NacosConfigProperties whose main file-extension defaults
  to "properties". Fall through directly to "properties" (historical
  default) and log a warning so the case is diagnosable.
- Use StringUtils#hasText instead of isEmpty so blank values are handled.
- Emit the effective fileExtension in the replace log for easier debugging.
- Add NacosPropertySourceRefreshListenerTest covering: yaml and json
  sources keep their format through refresh, a legacy source without
  extension falls back to "properties", and a missing dataId skips the
  refresh without calling the backing ConfigService.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant