Skip to content

[POC] Add validation for supported fields, Support customization of config_source#6721

Closed
jrhee17 wants to merge 1 commit intoline:mainfrom
jrhee17:feat/xds-support-field
Closed

[POC] Add validation for supported fields, Support customization of config_source#6721
jrhee17 wants to merge 1 commit intoline:mainfrom
jrhee17:feat/xds-support-field

Conversation

@jrhee17
Copy link
Copy Markdown
Contributor

@jrhee17 jrhee17 commented Apr 14, 2026

Motivation:

Explain why you're making this change and what problem you're trying to solve.

Modifications:

  • List the modifications you've made in detail.

Result:

  • Closes #. (If this resolves the issue.)
  • Describe the consequences that a user will face after this PR is merged.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive xDS validation and extension architecture refactor. It adds custom protobuf field option annotations across Envoy proto definitions, creates a new XdsExtensionRegistry-based factory pattern for discovering xDS extensions (filters, transport sockets, config sources), implements a stricter validation layer via PgvValidator and StrictXdsValidatorIndex, refactors resource parsing to use the extension registry, eliminates the monolithic ConfigSourceClient in favor of factory-created ConfigSourceSubscription and SubscriptionHandler abstractions, and updates stream handling to leverage the new extension architecture.

Changes

Cohort / File(s) Summary
Proto field annotations
xds-api/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto, xds-api/src/main/proto/envoy/config/cluster/v3/cluster.proto, xds-api/src/main/proto/envoy/config/core/v3/..., xds-api/src/main/proto/envoy/config/endpoint/v3/..., xds-api/src/main/proto/envoy/config/listener/v3/..., xds-api/src/main/proto/envoy/config/route/v3/..., xds-api/src/main/proto/envoy/extensions/...
Added armeria/xds/supported.proto imports and marked Armeria-supported xDS fields/enum values with (armeria.xds.supported) and (armeria.xds.supported_value) options across 20+ protobuf files.
Core validation API
xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java, xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java, xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java, xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
New PgvValidator and StrictXdsValidatorIndex implementing protobuf and Armeria-specific field validation; refactored DefaultXdsValidatorIndex to remove reflection-based pathway; updated XdsValidatorIndex contract to accept Message instead of Object. ServiceLoader entry added for StrictXdsValidatorIndex.
Extension registry system
xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java, xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionFactory.java, xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java, xds/src/main/java/com/linecorp/armeria/xds/HttpConnectionManagerFactory.java, xds/src/main/java/com/linecorp/armeria/xds/RawBufferTransportSocketFactory.java, xds/src/main/java/com/linecorp/armeria/xds/UpstreamTlsTransportSocketFactory.java
New extension discovery architecture: XdsExtensionRegistry (immutable registry with SPI loading), XdsExtensionFactory (base interface for extension factories), XdsResourceValidator (replaces static XdsValidatorIndexRegistry), and concrete factories for HTTP connection manager and transport sockets.
Subscription abstractions
xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceSubscription.java, xds/src/main/java/com/linecorp/armeria/xds/SubscriptionHandler.java, xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java, xds/src/main/java/com/linecorp/armeria/xds/SotwConfigSourceSubscriptionFactory.java, xds/src/main/java/com/linecorp/armeria/xds/SotwSubscriptionCallbacks.java, xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java, xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java, xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java
New subscription/config source handling: ConfigSourceSubscription interface, SubscriptionHandler abstraction replacing ConfigSourceClient, factory interfaces for creating subscriptions (SotwConfigSourceSubscriptionFactory, GrpcConfigSourceStreamFactory), callback handlers (SotwSubscriptionCallbacks, SubscriptionCallbacks), and path-based config source factory.
Resource parsing refactor
xds/src/main/java/com/linecorp/armeria/xds/ResourceParser.java, xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java, xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java, xds/src/main/java/com/linecorp/armeria/xds/ListenerResourceParser.java, xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java, xds/src/main/java/com/linecorp/armeria/xds/SecretResourceParser.java
Updated ResourceParser contract to accept XdsExtensionRegistry, version, and revision; resource parsers now accept registry for validation; removed direct XdsValidatorIndexRegistry validation calls in favor of registry-based validation.
Stream and subscription handling
xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java, xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java, xds/src/main/java/com/linecorp/armeria/xds/TransportSocketStream.java, xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java, xds/src/main/java/com/linecorp/armeria/xds/StateCoordinator.java
Refactored stream classes to use extension registry for resource parser resolution and callbacks; ControlPlaneClientManager now manages SubscriptionHandler instances via factories; StateCoordinator extended with ConfigSource-based timeout initialization.
Filter and transport socket handling
xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java, xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactory.java, xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java, xds/src/main/java/com/linecorp/armeria/xds/TransportSocketFactory.java, xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java
FilterUtil refactored to use extension registry for filter resolution; HttpFilterFactory extended with validator parameter; new TransportSocketFactory interface for transport socket extension discovery; XdsUnpackUtil for unpacking HttpConnectionManager and resolving downstream filters via registry.
Resource and listener handling
xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java, xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java, xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java, xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java, xds/src/main/java/com/linecorp/armeria/xds/EndpointXdsResource.java, xds/src/main/java/com/linecorp/armeria/xds/RouteXdsResource.java, xds/src/main/java/com/linecorp/armeria/xds/VirtualHostXdsResource.java, xds/src/main/java/com/linecorp/armeria/xds/SecretXdsResource.java
Removed validation calls from resource constructors; refactored ListenerXdsResource to accept explicit HttpConnectionManager and downstreamFilters; updated RouteEntry to accept extension registry for filter resolution.
Bootstrap and utility refactoring
xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapImpl.java, xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java, xds/src/main/java/com/linecorp/armeria/xds/DefaultSubscriptionContext.java, xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
XdsBootstrapImpl creates and wires XdsExtensionRegistry and validates bootstrap; XdsResourceParserUtil refactored to use immutable maps; DefaultSubscriptionContext stores and exposes extension registry; RouteEntry accepts registry for filter decoration.
Removed code
xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceClient.java (deleted), xds/src/main/java/com/linecorp/armeria/xds/XdsConverterUtil.java (deleted), xds/src/main/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistry.java (deleted), xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java (deleted)
Deleted monolithic ConfigSourceClient (replaced by factory-based ConfigSourceSubscription); deleted XdsConverterUtil validation logic (now in GrpcConfigSourceStreamFactory); deleted XdsValidatorIndexRegistry (replaced by XdsResourceValidator); deleted HttpFilterFactoryRegistry (replaced by extension registry lookup).
Tests
xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java, xds/src/test/java/com/linecorp/armeria/xds/XdsResourceValidatorTest.java, xds/src/test/java/com/linecorp/armeria/xds/XdsClientCleanupTest.java, xds/src/test/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistryTest.java (deleted), it/xds-client/src/test/resources/META-INF/services/..., it/xds-no-validation/src/test/java/..., xds-validator/build.gradle
New tests for XdsExtensionRegistry and XdsResourceValidator; updated existing tests to use new registry types; deleted old validator registry test; added protobuf dependency to xds-validator module; updated no-op validator signature.

Sequence Diagram

sequenceDiagram
    participant XdsBootstrap
    participant ExtRegistry as XdsExtensionRegistry
    participant ControlPlane as ControlPlaneClientManager
    participant SubFactory as ConfigSourceSubscription<br/>Factory
    participant StateCoord as StateCoordinator
    participant Stream as XdsStream
    participant Parser as ResourceParser
    participant Validator as XdsResourceValidator

    XdsBootstrap->>ExtRegistry: of(validator, watchService, ...)
    XdsBootstrap->>ExtRegistry: assertValid(bootstrap)
    XdsBootstrap->>ControlPlane: new(extensionRegistry)
    
    ControlPlane->>SubFactory: resolve factory by type/name
    SubFactory->>StateCoord: new(eventLoop, configSource, delta)
    SubFactory->>StateCoord: register callbacks
    SubFactory-->>ControlPlane: return ConfigSourceSubscription
    
    Stream->>Parser: fromTypeUrl(typeUrl)
    Stream->>Parser: parseResources(..., extensionRegistry, ...)
    Parser->>ExtRegistry: assertValid(unpackedMessage)
    Validator-->>Parser: validation result
    Parser-->>Stream: return XdsResource list
    
    Stream->>StateCoord: onResourceUpdated(resource)
    Stream->>StateCoord: onResourceMissing(name)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

new feature, breaking change, refactoring

Suggested reviewers

  • trustin
  • ikhoon

Poem

🐇 An extension registry hops in bright,
Factories bloom where validators write,
Protobuf fields now marked with care—
Strict xDS paths beyond compare!
Subscriptions dance in patterns new,
The bootstrapped world sings true! ✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@jrhee17 jrhee17 force-pushed the feat/xds-support-field branch from 89c2909 to 37b1b1c Compare April 14, 2026 02:42
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ConfigSourceLifecycleObserverTest.java (1)

531-578: ⚠️ Potential issue | 🟡 Minor

Static version increments during test discovery may cause flaky behavior.

The resourceRejection_args() method is annotated with @MethodSource and is called during JUnit test discovery. It increments the static version field (lines 558, 564, 570, 576) each time it's invoked. Since test discovery can happen multiple times or in different orders, this may lead to non-deterministic version strings in the snapshots.

Consider computing version strings inside the test method instead, or using a fresh version counter per test invocation.

🛠️ Suggested approach
 static Stream<Arguments> resourceRejection_args() {
     // Create valid resources
     ...
     return Stream.of(
-            Arguments.of(XdsType.LISTENER, Snapshot.create(
-                    ...
-                    ImmutableList.of(), String.valueOf(version.incrementAndGet()))),
+            Arguments.of(XdsType.LISTENER, /* pass resource objects, compute snapshot in test */),
         ...
     );
 }

Or pass the malformed resource type and construct the snapshot inside resourceRejection() using a fresh version increment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ConfigSourceLifecycleObserverTest.java`
around lines 531 - 578, The static version field is being incremented inside the
MethodSource provider resourceRejection_args(), causing non-deterministic
version strings during test discovery; move version increments out of the method
source by returning argument tuples that identify the malformed resource type
(e.g., enum or marker) rather than prebuilt Snapshot versions, and then inside
the test method resourceRejection(...) construct the Snapshot with
Snapshot.create(...) using a fresh version string generated there (e.g.,
version.incrementAndGet() or a local counter) so each test invocation computes
its own deterministic version instead of changing a shared static during
discovery.
🧹 Nitpick comments (5)
xds/src/main/java/com/linecorp/armeria/xds/ResourceStateStore.java (1)

76-85: Enforce type/resourceName invariants in putVersioned(...).

putVersioned(...) currently trusts caller-provided key/type without validating against resource. A mismatch can silently corrupt state buckets.

Proposed hardening diff
     `@Nullable`
     XdsResource putVersioned(XdsType type, String resourceName, XdsResource resource) {
+        if (resource.type() != type) {
+            throw new IllegalArgumentException(
+                    "Mismatched resource type: expected=" + type + ", actual=" + resource.type());
+        }
+        if (!resourceName.equals(resource.name())) {
+            throw new IllegalArgumentException(
+                    "Mismatched resource name: key=" + resourceName + ", actual=" + resource.name());
+        }
         final ResourceState prev = state(type, resourceName);
         if (isDuplicateEntry(resource, prev)) {
             return null;
         }
As per coding guidelines, keep exception messages meaningful (include offending values + expected constraints).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ResourceStateStore.java` around
lines 76 - 85, putVersioned(...) currently accepts a caller-supplied XdsType and
resourceName without verifying they match the provided XdsResource, which can
corrupt the per-type state buckets; before calling isDuplicateEntry(...)
validate that resource is non-null and that its identifying/type fields (use
XdsResource's accessor(s) — e.g. the resource's name/type or, for
AbstractXdsResource, the values returned by its getters) equal the supplied
resourceName and type, and if not throw an IllegalArgumentException that
includes both the offending values and the expected type/resourceName; perform
this check at the top of putVersioned (before computing revision and inserting
into statesFor(...)) so incorrect calls fail fast and preserve bucket
invariants.
xds/src/main/java/com/linecorp/armeria/xds/XdsResourceFactory.java (1)

47-65: Derive typeUrl() from type() to remove a drift point.

Every implementation here maps typeUrl() straight from the same XdsType. Keeping both abstract means one bad override can register a factory under the wrong URL while still reporting the right type.

Suggested change
     /**
      * Returns the type URL for this resource factory. Used as both the registry key
      * and the name.
      */
-    String typeUrl();
+    default String typeUrl() {
+        return type().typeUrl();
+    }

If a factory ever needs a different URL, it can still override this default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsResourceFactory.java` around
lines 47 - 65, The implementation exposes both abstract type() and typeUrl(),
which allows inconsistent overrides; change typeUrl() to be a default method
that derives its value from type() (i.e., return type().url() or however XdsType
exposes its URL) so the registry key/name and reported XdsType cannot drift, and
keep the ability for implementations to override typeUrl() if needed; update
XdsResourceFactory's typeUrl() default and leave name() and typeUrls() as-is to
continue using typeUrl().
xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java (1)

94-101: Silent overwriting of factories with duplicate typeUrls.

The register() method puts factories into the maps without checking for duplicates. If an SPI-loaded factory shares a typeUrl with a built-in factory (or another SPI factory), the later registration silently overwrites the earlier one. This could lead to unexpected behavior if multiple factories claim the same typeUrl.

Consider either logging a warning when overwriting occurs, or throwing an exception to detect conflicts early during initialization.

🔧 Proposed fix to detect duplicates
 private static void register(XdsExtensionFactory factory,
                              Map<String, XdsExtensionFactory> byName,
                              Map<String, XdsExtensionFactory> byTypeUrl) {
-    byName.put(factory.name(), factory);
+    final XdsExtensionFactory existingByName = byName.put(factory.name(), factory);
+    if (existingByName != null) {
+        logger.warn("Factory for name '{}' overwritten: {} -> {}",
+                    factory.name(), existingByName.getClass(), factory.getClass());
+    }
     for (String typeUrl : factory.typeUrls()) {
-        byTypeUrl.put(typeUrl, factory);
+        final XdsExtensionFactory existingByTypeUrl = byTypeUrl.put(typeUrl, factory);
+        if (existingByTypeUrl != null) {
+            logger.warn("Factory for typeUrl '{}' overwritten: {} -> {}",
+                        typeUrl, existingByTypeUrl.getClass(), factory.getClass());
+        }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java` around
lines 94 - 101, The register(...) helper currently blindly overwrites existing
entries in byName and byTypeUrl when an XdsExtensionFactory (from
XdsExtensionFactory.name() / typeUrls()) collides; update register to detect
duplicates by checking byTypeUrl.containsKey(typeUrl) and
byName.containsKey(factory.name()) for each factory, and either log a clear
warning (including both factory.name() and the existing factory identity) or
throw an IllegalStateException to fail fast on conflicts; apply this check for
every factory.typeUrls() and ensure the chosen behavior (warning vs exception)
is used consistently so SPI-loaded factories cannot silently replace built-ins.
xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java (1)

156-164: Consider using map key in error path for better debuggability.

The current implementation uses an index-based path like fieldPath[0].value, but for maps, the iteration order is not guaranteed to be meaningful. Including the actual map key in the path would make error messages more useful for debugging.

♻️ Suggested improvement
                     if (valueField != null &&
                         valueField.getJavaType() == FieldDescriptor.JavaType.MESSAGE &&
                         isAnnotatablePackage(valueField.getMessageType().getFile().getPackage())) {
-                        int i = 0;
+                        final FieldDescriptor keyField = fd.getMessageType().findFieldByName("key");
                         for (Message mapEntry : (List<Message>) value) {
                             final Object mapValue = mapEntry.getField(valueField);
                             if (mapValue instanceof Message) {
-                                doValidate((Message) mapValue, fieldPath + '[' + i + "].value");
+                                final Object mapKey = keyField != null ? mapEntry.getField(keyField) : null;
+                                final String keyStr = mapKey != null ? String.valueOf(mapKey) : "?";
+                                doValidate((Message) mapValue, fieldPath + '[' + keyStr + ']');
                             }
-                            i++;
                         }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java`
around lines 156 - 164, The loop in SupportedFieldValidator (inside doValidate)
uses an index for map entries which is unhelpful for maps; change the iteration
over Message mapEntry to extract the actual map key via
mapEntry.getField(keyField) (or equivalent key field used for map entries),
convert it to a string (handling nulls and non-String keys), and use that key in
the recursive path call (e.g., fieldPath + '[' + keyString + "].value"); if the
key cannot be obtained, fall back to the index to preserve behavior; update
references to valueField and mapEntry accordingly and ensure no NPEs when key is
missing.
xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java (1)

85-98: Consider simplifying the redundant condition check.

The condition on lines 87-88 checks both factory.isFullStateOfTheWorld() AND (type == LISTENER || type == CLUSTER). Since "full state of the world" semantics in xDS are specific to LDS and CDS types, this double-check appears redundant. If isFullStateOfTheWorld() correctly identifies these types, the explicit type check is unnecessary.

♻️ Suggested simplification
         // SotW absent detection for full-state types (LDS/CDS)
         final XdsResourceFactory factory =
                 extensionRegistry.queryByTypeUrl(type.typeUrl(), XdsResourceFactory.class);
-        if (factory != null && factory.isFullStateOfTheWorld() &&
-            (type == XdsType.LISTENER || type == XdsType.CLUSTER)) {
+        if (factory != null && factory.isFullStateOfTheWorld()) {
             final Set<String> currentSubscribers = stateCoordinator.interestedResources(type);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java` around
lines 85 - 98, Remove the redundant explicit type check and rely on
factory.isFullStateOfTheWorld() to determine full-state semantics: replace the
condition "factory != null && factory.isFullStateOfTheWorld() && (type ==
XdsType.LISTENER || type == XdsType.CLUSTER)" with "factory != null &&
factory.isFullStateOfTheWorld()". Leave the subsequent logic that fetches
currentSubscribers via stateCoordinator.interestedResources(type) and iterates
to call stateCoordinator.onResourceMissing(type, name) when parsedResources and
invalidResources do not contain the name; ensure parsedResources and
invalidResources checks remain intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DeltaXdsResourceWatcherTest.java`:
- Around line 261-268: The test currently only checks that errors is non-empty
after calling cache.setSnapshot(GROUP, Snapshot.create(..., "4")), which can
hide unrelated async failures; change the assertion in the await().untilAsserted
block to verify the specific deletion error type or message for cluster/endpoint
deletion rather than assertThat(errors).isNotEmpty(). Locate the
await().untilAsserted usage that wraps assertThat(errors) and replace the
predicate to assert that errors contains an entry matching the expected
exception/class/message (e.g., specific deletion error type or a known error
string) so the test validates the correct deletion behavior for the Snapshot
version "4".
- Around line 147-163: The test only waits for MissingXdsResourceException but
doesn't assert that no new snapshots were added; before calling
cache.setSnapshot(...) capture the current snapshots.size() (e.g.,
sizeBeforeRemoval), then after awaiting the MissingXdsResourceException assert
that snapshots.size() == sizeBeforeRemoval to ensure no new snapshot was
emitted, and optionally verify the last snapshot (or lack thereof) does not
contain a ListenerSnapshot for LISTENER_NAME; refer to
DeltaXdsResourceWatcherTest, cache.setSnapshot(...), errors,
MissingXdsResourceException, XdsType.LISTENER, LISTENER_NAME and snapshots when
making these checks.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandler.java`:
- Around line 53-57: In MeterUnsupportedFieldHandler, add an explicit null-check
at the start of the public method handle(String fieldPath) by calling
Objects.requireNonNull(fieldPath, "fieldPath") so fieldPath is validated before
being used to build tags (Counter.builder(...).tag("field", fieldPath)...); this
ensures the public parameter is non-null per coding guidelines and avoids NPEs
when registering the counter with meterRegistry.

In `@xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java`:
- Around line 49-52: The public method PgvValidator.assertValid(Object message)
must perform an explicit null check on the incoming parameter before delegating;
add Objects.requireNonNull(message, "message") at the start of assertValid to
enforce the contract and keep the existing try/catch (catching
ValidationException) and delegation to delegate.validatorFor(message). Ensure
the method throws a clear NPE immediately for null input rather than relying on
delegate behavior.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java`:
- Around line 40-46: The public method StrictXdsValidatorIndex.assertValid
currently allows a null message to pass to downstream validators; add an
explicit null-check using Objects.requireNonNull(message, "message") at the
start of assertValid to fail fast and give a stable error to callers before
calling pgvValidator.assertValid(message) and
SupportedFieldValidator.assertValid(message); update imports if needed to
reference java.util.Objects.

In `@xds-api/src/main/proto/envoy/config/cluster/v3/cluster.proto`:
- Around line 226-229: The enum value DEFAULT_SUBSET is annotated as supported
but its companion field google.protobuf.Struct default_subset is missing the
(armeria.xds.supported) annotation causing SupportedFieldValidator to reject
configs; update the default_subset field declaration in cluster.proto (and the
analogous default_subset in
extensions/load_balancing_policies/subset/v3/subset.proto) to include
[(armeria.xds.supported) = true], so the SupportedFieldValidator will treat the
nested field as supported when DEFAULT_SUBSET is used.

In `@xds/src/main/java/com/linecorp/armeria/xds/AdsXdsStream.java`:
- Around line 117-120: resourcesUpdated currently calls
ensureStream().resourcesUpdated(type) off the event loop, which can race with
stop()/retryOrClose() and mutate actualStream concurrently; change
resourcesUpdated(XdsType) to marshal its body onto the existing eventLoop (e.g.,
check eventLoop.inEventLoop and either run directly or schedule via
eventLoop.execute) so that ensureStream() and the subsequent
resourcesUpdated(type) invoke happen only on the eventLoop thread, preserving
the single-threaded invariants around actualStream, stop(), and retryOrClose().

In `@xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java`:
- Around line 45-47: The close() method currently calls stream.close() then
stateCoordinator.close(), which can skip closing the coordinator if
stream.close() throws; update ConfigSourceHandler.close() to ensure
stateCoordinator.close() always runs by wrapping stream.close() in a try block
with stateCoordinator.close() in a finally block (i.e., try { stream.close(); }
finally { stateCoordinator.close(); }) so both resources are guaranteed closed
even if ConfigSourceSubscription.close()/stream.close() throws.

In `@xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java`:
- Around line 88-95: The local close path flips the completed flag in
DeltaActualStream.closeStream() but onError() and onCompleted() still proceed to
notify the lifecycle observer and call owner.retryOrClose(...), causing
duplicate/undesired retry/close behavior; modify DeltaActualStream.onError(...)
and DeltaActualStream.onCompleted(...) to first check the completed boolean and
return immediately if true (i.e., ignore terminal callbacks after an intentional
local close), so they do not call lifecycleObserver.* or
owner.retryOrClose(...); apply the same guard to the analogous terminal-callback
handlers in the other block referenced (the second onError/onCompleted
implementations around the later range) to ensure consistent behavior.
- Around line 221-226: When DeltaActualStream finds factory == null for
extensionRegistry.queryByTypeUrl(value.getTypeUrl(), XdsResourceFactory.class),
do not return silently; treat it as a protocol error and NACK the response with
an explicit error message and the incoming nonce. Replace the early return in
DeltaActualStream with code that builds a descriptive error string containing
value.getTypeUrl() and value.getNonce(), logs it (logger.error/ warn as
appropriate), and calls the existing NACK path (e.g., the method that sends a
negative ACK for delta responses in this class or the control-plane sender) so
the ADS control plane receives the nonce + error details instead of leaving the
request unanswered.

In `@xds/src/main/java/com/linecorp/armeria/xds/DeltaDiscoveryStub.java`:
- Around line 41-65: The default branch in DeltaDiscoveryStub.basic(XdsType
type, GrpcClientBuilder builder) throws java.lang.Error for an unexpected enum
value; replace that with throwing an IllegalArgumentException (e.g. throw new
IllegalArgumentException("Unexpected value: " + type)) so callers can handle it
normally. Locate the switch in the basic(...) method and change the default
throw Error(...) to throw new IllegalArgumentException(...) preserving the
message content.

In
`@xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java`:
- Around line 97-104: The code currently only validates the first GrpcService
(firstGrpcService) for envoyGrpc but must validate every configured GrpcService:
iterate over apiConfigSource.getGrpcServicesList() (or stream it) and assert
each GrpcService.hasEnvoyGrpc() using the same checkArgument-style message
(include which configSource or index for clarity), failing if any entry is not
envoyGrpc; after that keep using grpcServices.get(0).getEnvoyGrpc() as before
(or assign envoyGrpc from the first element) so behavior is unchanged except
that all entries are validated.

In
`@xds/src/main/java/com/linecorp/armeria/xds/HttpConnectionManagerFactory.java`:
- Around line 29-32: The NAME constant in HttpConnectionManagerFactory is using
the old v2 identifier and should be updated to the canonical Envoy v3 extension
name; locate the static field NAME in class HttpConnectionManagerFactory and
change its value from "envoy.http_connection_manager" to
"envoy.filters.network.http_connection_manager" (leave TYPE_URL unchanged) so
name-based registry lookups (e.g., queryByName()) will succeed and remain
consistent with other factories.

In
`@xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java`:
- Around line 110-112: The code in PathSotwConfigSourceSubscriptionFactory
builds a DiscoveryResponse by parsing JSON from a byte array using new
String(bytes), which relies on the platform default charset; change the
conversion to use an explicit charset (e.g., StandardCharsets.UTF_8) when
calling JsonFormat.parser().merge so the JSON is always decoded as UTF-8, and
add the necessary import for java.nio.charset.StandardCharsets.

In `@xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java`:
- Around line 89-96: closeStream() sets completed = true but onError() and
onCompleted() still call retryOrClose(...), causing an intentional local close
to be treated as a remote termination; update onError(...) and onCompleted(...)
in SotwActualStream to first check the completed flag and return immediately if
true (same pattern used in the delta implementation), so that terminal callbacks
are ignored after closeStream() and do not invoke retryOrClose(...); apply the
same guard to the other occurrence of these handlers in this class.
- Around line 145-149: SotwActualStream currently logs and returns when
extensionRegistry.queryByTypeUrl(value.getTypeUrl(), XdsResourceFactory.class)
yields null, which drops the response without ACK/NACK; instead, treat this as a
protocol error by sending a NACK for the received DiscoveryResponse (including
value.getTypeUrl() and a clear error message) back to the management server and
then return. Locate the null-check around XdsResourceFactory factory in
SotwActualStream, replace the silent return with logic that builds/sends a NACK
for the incoming response (use the stream's existing ACK/NACK helper or call the
method that sends discovery-response errors), include the typeUrl and the reason
string in the NACK, and keep the logger.warn for visibility. Ensure you
reference the same value (value.getTypeUrl()) and
extensionRegistry.queryByTypeUrl semantics when constructing the NACK so the
server receives an explicit protocol failure for unknown types.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java`:
- Around line 76-85: The public method unpack(Any message, Class<T> clazz) lacks
null checks for its parameters; add explicit checks at the start of the method
using Objects.requireNonNull(message, "message") and
Objects.requireNonNull(clazz, "clazz") so callers get clear NPEs and the method
never passes null into message.unpack or assertValid(unpacked).

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java`:
- Around line 36-44: Replace the unsafe assert in XdsUnpackUtil: after obtaining
apiListener and calling registry.queryByTypeUrl(...,
HttpConnectionManagerFactory.class) check if the returned
HttpConnectionManagerFactory (the variable factory) is null and throw a clear
exception (e.g., IllegalStateException or a custom descriptive exception) or
return a handled error instead of relying on assert; ensure the null-check
message references the apiListener.getTypeUrl() and that
factory.create(apiListener, registry.validator()) is only invoked when factory
is non-null.

In `@xds/src/test/java/com/linecorp/armeria/xds/SubscriberStorageTest.java`:
- Around line 68-70: The test is racy because watcher.missingType and
watcher.missingName are set on the event-loop thread and the test waits only for
missingType then reads missingName separately; fix it by making the read happen
inside the same await().untilAsserted() to ensure both watcher.missingType and
watcher.missingName are observed atomically from the test thread (or
alternatively make the fields thread-safe like volatile/AtomicReference), e.g.
assert both watcher.missingType and watcher.missingName within the same
untilAsserted lambda (also apply the same change for the later assertions
covering the other case).

In `@xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java`:
- Around line 34-41: Add a static teardown to close the DirectoryWatchService
static field: in XdsExtensionRegistryTest add a public static method (e.g.,
tearDown or cleanup) annotated with `@AfterClass` that calls WATCH_SERVICE.close()
(or SafeCloseable.close()) to release the resource after all tests; ensure the
method is static and handles/propagates any checked exceptions as required by
the test framework so WATCH_SERVICE is properly closed.

---

Outside diff comments:
In
`@it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ConfigSourceLifecycleObserverTest.java`:
- Around line 531-578: The static version field is being incremented inside the
MethodSource provider resourceRejection_args(), causing non-deterministic
version strings during test discovery; move version increments out of the method
source by returning argument tuples that identify the malformed resource type
(e.g., enum or marker) rather than prebuilt Snapshot versions, and then inside
the test method resourceRejection(...) construct the Snapshot with
Snapshot.create(...) using a fresh version string generated there (e.g.,
version.incrementAndGet() or a local counter) so each test invocation computes
its own deterministic version instead of changing a shared static during
discovery.

---

Nitpick comments:
In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java`:
- Around line 156-164: The loop in SupportedFieldValidator (inside doValidate)
uses an index for map entries which is unhelpful for maps; change the iteration
over Message mapEntry to extract the actual map key via
mapEntry.getField(keyField) (or equivalent key field used for map entries),
convert it to a string (handling nulls and non-String keys), and use that key in
the recursive path call (e.g., fieldPath + '[' + keyString + "].value"); if the
key cannot be obtained, fall back to the index to preserve behavior; update
references to valueField and mapEntry accordingly and ensure no NPEs when key is
missing.

In `@xds/src/main/java/com/linecorp/armeria/xds/ResourceStateStore.java`:
- Around line 76-85: putVersioned(...) currently accepts a caller-supplied
XdsType and resourceName without verifying they match the provided XdsResource,
which can corrupt the per-type state buckets; before calling
isDuplicateEntry(...) validate that resource is non-null and that its
identifying/type fields (use XdsResource's accessor(s) — e.g. the resource's
name/type or, for AbstractXdsResource, the values returned by its getters) equal
the supplied resourceName and type, and if not throw an IllegalArgumentException
that includes both the offending values and the expected type/resourceName;
perform this check at the top of putVersioned (before computing revision and
inserting into statesFor(...)) so incorrect calls fail fast and preserve bucket
invariants.

In `@xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java`:
- Around line 85-98: Remove the redundant explicit type check and rely on
factory.isFullStateOfTheWorld() to determine full-state semantics: replace the
condition "factory != null && factory.isFullStateOfTheWorld() && (type ==
XdsType.LISTENER || type == XdsType.CLUSTER)" with "factory != null &&
factory.isFullStateOfTheWorld()". Leave the subsequent logic that fetches
currentSubscribers via stateCoordinator.interestedResources(type) and iterates
to call stateCoordinator.onResourceMissing(type, name) when parsedResources and
invalidResources do not contain the name; ensure parsedResources and
invalidResources checks remain intact.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java`:
- Around line 94-101: The register(...) helper currently blindly overwrites
existing entries in byName and byTypeUrl when an XdsExtensionFactory (from
XdsExtensionFactory.name() / typeUrls()) collides; update register to detect
duplicates by checking byTypeUrl.containsKey(typeUrl) and
byName.containsKey(factory.name()) for each factory, and either log a clear
warning (including both factory.name() and the existing factory identity) or
throw an IllegalStateException to fail fast on conflicts; apply this check for
every factory.typeUrls() and ensure the chosen behavior (warning vs exception)
is used consistently so SPI-loaded factories cannot silently replace built-ins.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsResourceFactory.java`:
- Around line 47-65: The implementation exposes both abstract type() and
typeUrl(), which allows inconsistent overrides; change typeUrl() to be a default
method that derives its value from type() (i.e., return type().url() or however
XdsType exposes its URL) so the registry key/name and reported XdsType cannot
drift, and keep the ability for implementations to override typeUrl() if needed;
update XdsResourceFactory's typeUrl() default and leave name() and typeUrls()
as-is to continue using typeUrl().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e49ce6d5-1efc-4f35-8f56-f9f940188a3c

📥 Commits

Reviewing files that changed from the base of the PR and between 233b5d8 and 37b1b1c.

📒 Files selected for processing (120)
  • it/xds-client/src/test/java/com/linecorp/armeria/xds/it/ConfigSourceLifecycleObserverTest.java
  • it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DeltaXdsPreprocessorTest.java
  • it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DeltaXdsResourceWatcherTest.java
  • it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsControlPlaneErrorHandlingTest.java
  • it/xds-client/src/test/java/com/linecorp/armeria/xds/it/XdsControlPlaneMatrixTest.java
  • it/xds-client/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • testing-internal/src/main/java/com/linecorp/armeria/internal/testing/InternalTestingBlockHoundIntegration.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/IgnoreUnsupportedFieldHandler.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandler.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/UnsupportedFieldHandler.java
  • xds-api/src/main/proto/armeria/xds/supported.proto
  • xds-api/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto
  • xds-api/src/main/proto/envoy/config/cluster/v3/cluster.proto
  • xds-api/src/main/proto/envoy/config/core/v3/address.proto
  • xds-api/src/main/proto/envoy/config/core/v3/base.proto
  • xds-api/src/main/proto/envoy/config/core/v3/config_source.proto
  • xds-api/src/main/proto/envoy/config/core/v3/grpc_service.proto
  • xds-api/src/main/proto/envoy/config/core/v3/health_check.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint_components.proto
  • xds-api/src/main/proto/envoy/config/listener/v3/api_listener.proto
  • xds-api/src/main/proto/envoy/config/listener/v3/listener.proto
  • xds-api/src/main/proto/envoy/config/route/v3/route.proto
  • xds-api/src/main/proto/envoy/config/route/v3/route_components.proto
  • xds-api/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto
  • xds-api/src/main/proto/envoy/type/matcher/v3/regex.proto
  • xds-api/src/main/proto/envoy/type/matcher/v3/string.proto
  • xds-api/src/main/proto/envoy/type/v3/percent.proto
  • xds-api/src/main/proto/envoy/type/v3/range.proto
  • xds-api/src/test/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandlerTest.java
  • xds-api/src/test/java/com/linecorp/armeria/xds/api/SupportedFieldValidatorTest.java
  • xds-api/src/test/proto/armeria/xds/testing/test_supported.proto
  • xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
  • xds/src/main/java/com/linecorp/armeria/xds/AbstractXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/AdsXdsStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/CompositeXdsStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceClient.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceLifecycleObserver.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceSubscription.java
  • xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/DefaultConfigSourceLifecycleObserver.java
  • xds/src/main/java/com/linecorp/armeria/xds/DefaultResponseHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/DefaultSubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/DeltaDiscoveryStub.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/HttpConnectionManagerFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerResourceFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/RawBufferTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceNodeAdapter.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceStateStore.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretResourceFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwSubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwXdsStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/StateCoordinator.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriberStorage.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/UpstreamTlsTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/VirtualHostXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapImpl.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsClusterManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsConverterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceException.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParseUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResponseHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsStreamSubscriber.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java
  • xds/src/test/java/com/linecorp/armeria/xds/SotwXdsStreamTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/StateCoordinatorTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/SubscriberStorageTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/TestResourceWatcher.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsClientCleanupTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsClientIntegrationTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsResourceValidatorTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistryTest.java
  • xds/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
💤 Files with no reviewable changes (15)
  • xds/src/test/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistryTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResponseHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java
  • xds/src/test/java/com/linecorp/armeria/xds/SotwXdsStreamTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwXdsStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/DefaultResponseHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsConverterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceClient.java

Comment on lines +147 to +163
cache.setSnapshot(GROUP, Snapshot.create(
ImmutableList.of(staticClusterYaml(CLUSTER_NAME)),
ImmutableList.of(),
ImmutableList.of(),
ImmutableList.of(routeYaml(ROUTE_NAME, CLUSTER_NAME)),
ImmutableList.of(),
"3"));
await().untilAsserted(() -> assertThat(errors).anyMatch(error -> {
if (!(error instanceof MissingXdsResourceException)) {
return false;
}
final MissingXdsResourceException exception = (MissingXdsResourceException) error;
return exception.type() == XdsType.LISTENER &&
exception.name().equals(LISTENER_NAME);
}));
final int sizeAfterRemoval = snapshots.size();

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.

⚠️ Potential issue | 🟡 Minor

Deletion step doesn't verify the “no new snapshot” contract.

This only waits for a MissingXdsResourceException and then snapshots the current size. If deletion accidentally emits another ListenerSnapshot, the test still passes and the regression slips through.

Suggested tightening
-            cache.setSnapshot(GROUP, Snapshot.create(
+            final int sizeBeforeRemoval = snapshots.size();
+            cache.setSnapshot(GROUP, Snapshot.create(
                     ImmutableList.of(staticClusterYaml(CLUSTER_NAME)),
                     ImmutableList.of(),
                     ImmutableList.of(),
                     ImmutableList.of(routeYaml(ROUTE_NAME, CLUSTER_NAME)),
                     ImmutableList.of(),
                     "3"));
-            await().untilAsserted(() -> assertThat(errors).anyMatch(error -> {
-                if (!(error instanceof MissingXdsResourceException)) {
-                    return false;
-                }
-                final MissingXdsResourceException exception = (MissingXdsResourceException) error;
-                return exception.type() == XdsType.LISTENER &&
-                       exception.name().equals(LISTENER_NAME);
-            }));
-            final int sizeAfterRemoval = snapshots.size();
+            await().untilAsserted(() -> {
+                assertThat(errors).anyMatch(error -> {
+                    if (!(error instanceof MissingXdsResourceException)) {
+                        return false;
+                    }
+                    final MissingXdsResourceException exception =
+                            (MissingXdsResourceException) error;
+                    return exception.type() == XdsType.LISTENER &&
+                           exception.name().equals(LISTENER_NAME);
+                });
+                assertThat(snapshots).hasSize(sizeBeforeRemoval);
+            });
+            final int sizeAfterRemoval = sizeBeforeRemoval;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@it/xds-client/src/test/java/com/linecorp/armeria/xds/it/DeltaXdsResourceWatcherTest.java`
around lines 147 - 163, The test only waits for MissingXdsResourceException but
doesn't assert that no new snapshots were added; before calling
cache.setSnapshot(...) capture the current snapshots.size() (e.g.,
sizeBeforeRemoval), then after awaiting the MissingXdsResourceException assert
that snapshots.size() == sizeBeforeRemoval to ensure no new snapshot was
emitted, and optionally verify the last snapshot (or lack thereof) does not
contain a ListenerSnapshot for LISTENER_NAME; refer to
DeltaXdsResourceWatcherTest, cache.setSnapshot(...), errors,
MissingXdsResourceException, XdsType.LISTENER, LISTENER_NAME and snapshots when
making these checks.

Comment on lines +53 to +57
public void handle(String fieldPath) {
final Counter counter = Counter.builder(meterIdPrefix.name("unsupported.fields"))
.tags(meterIdPrefix.tags())
.tag("field", fieldPath)
.register(meterRegistry);
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.

⚠️ Potential issue | 🟡 Minor

Add explicit null-check for fieldPath in the public handle(...) method.

At Line 53, fieldPath should be validated with requireNonNull before using it to build tags.

Suggested fix
 `@Override`
 public void handle(String fieldPath) {
+    fieldPath = requireNonNull(fieldPath, "fieldPath");
     final Counter counter = Counter.builder(meterIdPrefix.name("unsupported.fields"))
                                    .tags(meterIdPrefix.tags())
                                    .tag("field", fieldPath)
                                    .register(meterRegistry);
     counter.increment();
 }

As per coding guidelines: "Do explicit null checks on user-facing public parameters using Objects.requireNonNull(obj, "name")".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandler.java`
around lines 53 - 57, In MeterUnsupportedFieldHandler, add an explicit
null-check at the start of the public method handle(String fieldPath) by calling
Objects.requireNonNull(fieldPath, "fieldPath") so fieldPath is validated before
being used to build tags (Counter.builder(...).tag("field", fieldPath)...); this
ensures the public parameter is non-null per coding guidelines and avoids NPEs
when registering the counter with meterRegistry.

Comment on lines +49 to +52
public void assertValid(Object message) {
try {
delegate.validatorFor(message).assertValid(message);
} catch (ValidationException e) {
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "PgvValidator.java" -type f

Repository: line/armeria

Length of output: 126


🏁 Script executed:

cat -n xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java

Repository: line/armeria

Length of output: 2173


Add explicit null validation in public assertValid API.

The assertValid(Object message) method must include an explicit null check per coding guidelines: "Do explicit null checks on user-facing public parameters using Objects.requireNonNull(obj, "name")." Currently, null handling is delegated indirectly, making the error contract non-deterministic.

Proposed fix
+import java.util.Objects;
+
 import com.linecorp.armeria.common.annotation.UnstableApi;
@@
     public void assertValid(Object message) {
+        Objects.requireNonNull(message, "message");
         try {
             delegate.validatorFor(message).assertValid(message);
         } catch (ValidationException e) {
             throw new IllegalArgumentException(e);
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java` around
lines 49 - 52, The public method PgvValidator.assertValid(Object message) must
perform an explicit null check on the incoming parameter before delegating; add
Objects.requireNonNull(message, "message") at the start of assertValid to
enforce the contract and keep the existing try/catch (catching
ValidationException) and delegation to delegate.validatorFor(message). Ensure
the method throws a clear NPE immediately for null input rather than relying on
delegate behavior.

Comment thread xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java Outdated
Comment on lines +145 to +149
final XdsResourceFactory factory =
extensionRegistry.queryByTypeUrl(value.getTypeUrl(), XdsResourceFactory.class);
if (factory == null) {
logger.warn("XDS stream Received unexpected type: {}", value.getTypeUrl());
return;
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.

⚠️ Potential issue | 🟠 Major

Don't silently drop unknown SotW response types.

If the client can't resolve typeUrl, logging and returning means the management server never gets an ACK/NACK for that response. This should be treated as a protocol failure, not ignored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java` around
lines 145 - 149, SotwActualStream currently logs and returns when
extensionRegistry.queryByTypeUrl(value.getTypeUrl(), XdsResourceFactory.class)
yields null, which drops the response without ACK/NACK; instead, treat this as a
protocol error by sending a NACK for the received DiscoveryResponse (including
value.getTypeUrl() and a clear error message) back to the management server and
then return. Locate the null-check around XdsResourceFactory factory in
SotwActualStream, replace the silent return with logic that builds/sends a NACK
for the incoming response (use the stream's existing ACK/NACK helper or call the
method that sends discovery-response errors), include the typeUrl and the reason
string in the NACK, and keep the logger.warn for visibility. Ensure you
reference the same value (value.getTypeUrl()) and
extensionRegistry.queryByTypeUrl semantics when constructing the NACK so the
server receives an explicit protocol failure for unknown types.

Comment on lines +36 to +44
if (listener.getApiListener().hasApiListener()) {
final Any apiListener = listener.getApiListener().getApiListener();
final HttpConnectionManagerFactory factory =
registry.queryByTypeUrl(apiListener.getTypeUrl(),
HttpConnectionManagerFactory.class);
assert factory != null;
return factory.create(apiListener, registry.validator());
}
return null;
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.

⚠️ Potential issue | 🟠 Major

Replace assert with explicit null check.

Using assert factory != null is unsafe because assertions can be disabled at runtime (-da flag). If the type URL is unregistered, this silently passes in production, leading to an NPE at factory.create(...).

🛡️ Proposed fix
         if (listener.getApiListener().hasApiListener()) {
             final Any apiListener = listener.getApiListener().getApiListener();
             final HttpConnectionManagerFactory factory =
                     registry.queryByTypeUrl(apiListener.getTypeUrl(),
                                             HttpConnectionManagerFactory.class);
-            assert factory != null;
+            if (factory == null) {
+                throw new IllegalArgumentException(
+                        "No HttpConnectionManagerFactory registered for type URL: " +
+                        apiListener.getTypeUrl());
+            }
             return factory.create(apiListener, registry.validator());
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (listener.getApiListener().hasApiListener()) {
final Any apiListener = listener.getApiListener().getApiListener();
final HttpConnectionManagerFactory factory =
registry.queryByTypeUrl(apiListener.getTypeUrl(),
HttpConnectionManagerFactory.class);
assert factory != null;
return factory.create(apiListener, registry.validator());
}
return null;
if (listener.getApiListener().hasApiListener()) {
final Any apiListener = listener.getApiListener().getApiListener();
final HttpConnectionManagerFactory factory =
registry.queryByTypeUrl(apiListener.getTypeUrl(),
HttpConnectionManagerFactory.class);
if (factory == null) {
throw new IllegalArgumentException(
"No HttpConnectionManagerFactory registered for type URL: " +
apiListener.getTypeUrl());
}
return factory.create(apiListener, registry.validator());
}
return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java` around lines
36 - 44, Replace the unsafe assert in XdsUnpackUtil: after obtaining apiListener
and calling registry.queryByTypeUrl(..., HttpConnectionManagerFactory.class)
check if the returned HttpConnectionManagerFactory (the variable factory) is
null and throw a clear exception (e.g., IllegalStateException or a custom
descriptive exception) or return a handled error instead of relying on assert;
ensure the null-check message references the apiListener.getTypeUrl() and that
factory.create(apiListener, registry.validator()) is only invoked when factory
is non-null.

Comment thread xds/src/test/java/com/linecorp/armeria/xds/SubscriberStorageTest.java Outdated
Comment thread xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java (2)

43-47: Add null checks for constructor parameters.

Per coding guidelines, constructor parameters should be validated with Objects.requireNonNull().

Proposed fix
+import static java.util.Objects.requireNonNull;
+
 SubscriptionCallbacks(StateCoordinator stateCoordinator,
-                          XdsExtensionRegistry extensionRegistry) {
-    this.stateCoordinator = stateCoordinator;
-    this.extensionRegistry = extensionRegistry;
+                      XdsExtensionRegistry extensionRegistry) {
+    this.stateCoordinator = requireNonNull(stateCoordinator, "stateCoordinator");
+    this.extensionRegistry = requireNonNull(extensionRegistry, "extensionRegistry");
 }

As per coding guidelines: "add null checks for user-facing public methods using Objects.requireNonNull(..., \"paramName\")".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java` around
lines 43 - 47, Add null checks in the SubscriptionCallbacks constructor by
validating its parameters using Objects.requireNonNull: call
Objects.requireNonNull(stateCoordinator, "stateCoordinator") and
Objects.requireNonNull(extensionRegistry, "extensionRegistry") before assigning
to the fields stateCoordinator and extensionRegistry so the constructor fails
fast on null inputs.

98-100: Unused parameter systemVersionInfo.

The systemVersionInfo parameter is declared but never used in the method body. If this is intentionally reserved for future use or API consistency with delta protocol, consider adding a brief comment. Otherwise, consider removing it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java` around
lines 98 - 100, The method SubscriptionCallbacks.onConfigUpdate currently
declares a parameter systemVersionInfo that is unused; either remove the
parameter from the onConfigUpdate signature (and update all callers and
implementations) or, if it must remain for API compatibility, add a brief inline
comment in the onConfigUpdate declaration explaining it is intentionally
reserved for future/delta-protocol use (e.g., "reserved for future use" or "kept
for API compatibility") and annotate with `@SuppressWarnings`("unused") if your
style permits to avoid warnings; update any implementing classes to match the
chosen approach.
xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java (2)

87-94: Silent overwriting of factories may mask configuration errors.

register() silently overwrites existing factory entries. If an SPI-loaded factory shares a name or typeUrl with a built-in, the built-in wins without warning. Consider logging a warning or throwing if a duplicate is detected.

Proposed fix to detect duplicates
 private static void register(XdsExtensionFactory factory,
                              Map<String, XdsExtensionFactory> byName,
                              Map<String, XdsExtensionFactory> byTypeUrl) {
-    byName.put(factory.name(), factory);
+    final XdsExtensionFactory existing = byName.put(factory.name(), factory);
+    if (existing != null) {
+        // Log or handle duplicate - built-in overriding SPI is intentional
+    }
     for (String typeUrl : factory.typeUrls()) {
         byTypeUrl.put(typeUrl, factory);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java` around
lines 87 - 94, The register method currently overwrites existing entries in
byName/byTypeUrl without notice; update XdsExtensionRegistry.register to detect
duplicates before inserting: check byName.containsKey(factory.name()) and
byTypeUrl.containsKey(typeUrl) for each factory.typeUrls(), and either log a
warning including the conflicting key and existing factory or throw an
IllegalStateException depending on policy; use the existing process/logger used
elsewhere in this class (or a static logger) and reference factory.name(),
factory.typeUrls(), byName and byTypeUrl when building the diagnostic message so
duplicate SPI vs built-in registrations are surfaced instead of silently
overwritten.

47-53: Add null checks for constructor parameters.

Per coding guidelines, user-facing or API-level constructor parameters should validate nulls. Even though this is package-private, defensive null checks prevent obscure NPEs downstream.

Proposed fix
+import static java.util.Objects.requireNonNull;
+
 private XdsExtensionRegistry(Map<String, XdsExtensionFactory> byTypeUrl,
                              Map<String, XdsExtensionFactory> byName,
                              XdsResourceValidator validator) {
-    this.byTypeUrl = byTypeUrl;
-    this.byName = byName;
-    this.validator = validator;
+    this.byTypeUrl = requireNonNull(byTypeUrl, "byTypeUrl");
+    this.byName = requireNonNull(byName, "byName");
+    this.validator = requireNonNull(validator, "validator");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java` around
lines 47 - 53, The XdsExtensionRegistry constructor should validate its
parameters to avoid later NPEs: in the XdsExtensionRegistry(Map<String,
XdsExtensionFactory> byTypeUrl, Map<String, XdsExtensionFactory> byName,
XdsResourceValidator validator) constructor, add null checks for byTypeUrl,
byName, and validator (e.g., via Objects.requireNonNull or explicit checks) and
throw a clear NullPointerException with a message identifying the missing
parameter before assigning to this.byTypeUrl, this.byName, and this.validator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java`:
- Around line 43-47: Add null checks in the SubscriptionCallbacks constructor by
validating its parameters using Objects.requireNonNull: call
Objects.requireNonNull(stateCoordinator, "stateCoordinator") and
Objects.requireNonNull(extensionRegistry, "extensionRegistry") before assigning
to the fields stateCoordinator and extensionRegistry so the constructor fails
fast on null inputs.
- Around line 98-100: The method SubscriptionCallbacks.onConfigUpdate currently
declares a parameter systemVersionInfo that is unused; either remove the
parameter from the onConfigUpdate signature (and update all callers and
implementations) or, if it must remain for API compatibility, add a brief inline
comment in the onConfigUpdate declaration explaining it is intentionally
reserved for future/delta-protocol use (e.g., "reserved for future use" or "kept
for API compatibility") and annotate with `@SuppressWarnings`("unused") if your
style permits to avoid warnings; update any implementing classes to match the
chosen approach.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java`:
- Around line 87-94: The register method currently overwrites existing entries
in byName/byTypeUrl without notice; update XdsExtensionRegistry.register to
detect duplicates before inserting: check byName.containsKey(factory.name()) and
byTypeUrl.containsKey(typeUrl) for each factory.typeUrls(), and either log a
warning including the conflicting key and existing factory or throw an
IllegalStateException depending on policy; use the existing process/logger used
elsewhere in this class (or a static logger) and reference factory.name(),
factory.typeUrls(), byName and byTypeUrl when building the diagnostic message so
duplicate SPI vs built-in registrations are surfaced instead of silently
overwritten.
- Around line 47-53: The XdsExtensionRegistry constructor should validate its
parameters to avoid later NPEs: in the XdsExtensionRegistry(Map<String,
XdsExtensionFactory> byTypeUrl, Map<String, XdsExtensionFactory> byName,
XdsResourceValidator validator) constructor, add null checks for byTypeUrl,
byName, and validator (e.g., via Objects.requireNonNull or explicit checks) and
throw a clear NullPointerException with a message identifying the missing
parameter before assigning to this.byTypeUrl, this.byName, and this.validator.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6c8280e1-5953-4bef-b6ce-c25d2920bfe6

📥 Commits

Reviewing files that changed from the base of the PR and between 37b1b1c and 2631fa8.

📒 Files selected for processing (11)
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java
✅ Files skipped from review due to trivial changes (1)
  • xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (8150425) to head (1ed8732).
⚠️ Report is 432 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main   #6721       +/-   ##
============================================
- Coverage     74.46%       0   -74.47%     
============================================
  Files          1963       0     -1963     
  Lines         82437       0    -82437     
  Branches      10764       0    -10764     
============================================
- Hits          61385       0    -61385     
+ Misses        15918       0    -15918     
+ Partials       5134       0     -5134     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jrhee17 jrhee17 force-pushed the feat/xds-support-field branch from 2631fa8 to 1e93d34 Compare April 14, 2026 07:20
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (4)
xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java (1)

19-20: ⚠️ Potential issue | 🟡 Minor

Still missing a null guard at the public API boundary.

assertValid(null) still falls through to downstream validators instead of failing fast from this new public API.

Suggested fix
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
@@
     `@Override`
     public void assertValid(Object message) {
+        Objects.requireNonNull(message, "message");
         pgvValidator.assertValid(message);
         final List<String> violations = new ArrayList<>();
As per coding guidelines, "Use null-safety conventions: explicit requireNonNull checks for user-facing public methods."

Also applies to: 40-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java`
around lines 19 - 20, StrictXdsValidatorIndex's public API lacks a null-guard:
add an explicit null check (e.g., Objects.requireNonNull(...)) at the start of
the public assertValid(...) method(s) in class StrictXdsValidatorIndex so
assertValid(null) throws immediately rather than delegating to downstream
validators; place the requireNonNull call at the top of the public method(s)
referenced (assertValid) to enforce null-safety for the API boundary.
xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java (1)

97-103: ⚠️ Potential issue | 🟠 Major

Validate every configured GrpcService.

Lines 100-102 only check the first entry, so a config with grpc_services[0].envoy_grpc and an unsupported fallback entry still passes this gate.

Proposed fix
             final List<GrpcService> grpcServices = apiConfigSource.getGrpcServicesList();
             checkArgument(!grpcServices.isEmpty(),
                           "At least one GrpcService should be specified for '%s'", configSource);
+            for (GrpcService grpcService : grpcServices) {
+                checkArgument(grpcService.hasEnvoyGrpc(),
+                              "Only envoyGrpc is supported for '%s'", configSource);
+            }
             final GrpcService firstGrpcService = grpcServices.get(0);
-            checkArgument(firstGrpcService.hasEnvoyGrpc(),
-                          "Only envoyGrpc is supported for '%s'", configSource);
             final EnvoyGrpc envoyGrpc = firstGrpcService.getEnvoyGrpc();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java`
around lines 97 - 103, The code only validates grpc_services[0]; instead iterate
apiConfigSource.getGrpcServicesList() and assert each GrpcService.hasEnvoyGrpc()
(e.g., for (GrpcService gs : grpcServices) checkArgument(gs.hasEnvoyGrpc(),
"Only envoyGrpc is supported for '%s'", configSource)); then keep the existing
firstGrpcService/getEnvoyGrpc() usage to obtain envoyGrpc after the per-entry
validation. This ensures every configured GrpcService is validated, not just the
first.
xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java (2)

153-172: ⚠️ Potential issue | 🟠 Major

Ignore terminal callbacks after closeStream().

closeStream() marks the stream completed on Line 94, but later onError() / onCompleted() still drive retryOrClose(...). That turns an intentional local shutdown into a reconnect.

Proposed fix
     `@Override`
     public void onError(Throwable throwable) {
         if (!eventLoop.inEventLoop()) {
             eventLoop.execute(() -> onError(throwable));
             return;
         }
+        if (completed) {
+            return;
+        }
         completed = true;
         lifecycleObserver.streamError(throwable);
         owner.retryOrClose(true);
     }

     `@Override`
     public void onCompleted() {
         if (!eventLoop.inEventLoop()) {
             eventLoop.execute(this::onCompleted);
             return;
         }
+        if (completed) {
+            return;
+        }
         completed = true;
         lifecycleObserver.streamCompleted();
         owner.retryOrClose(false);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java` around
lines 153 - 172, The onError/onCompleted paths must ignore terminal callbacks if
closeStream() already marked the stream completed; add an early guard in both
onError(Throwable) and onCompleted() that returns immediately when completed is
true (before scheduling onto eventLoop) so you don't reschedule work or call
lifecycleObserver.streamError/streamCompleted or owner.retryOrClose(...) after a
local close; reference the existing completed flag, the onError/onCompleted
methods, lifecycleObserver, owner.retryOrClose, and closeStream() when making
this change.

145-149: ⚠️ Potential issue | 🟠 Major

NACK unknown SotW response types instead of dropping them.

Logging and returning here means the management server never gets an ACK/NACK for that response. This should be treated as a protocol error, not ignored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java` around
lines 145 - 149, The code in SotwActualStream currently logs and returns when
XdsResourceParserUtil.fromTypeUrl(value.getTypeUrl()) returns null, which drops
the response; instead construct and send a NACK for this DiscoveryResponse:
build an error Status (e.g., INVALID_ARGUMENT) with a descriptive message
including value.getTypeUrl(), then call the class's existing NACK pathway using
the DiscoveryResponse identifiers (value.getVersionInfo(), value.getNonce(), and
optionally resource names from value.getResourcesList()) — reuse the
SotwActualStream method that sends NACKs (the existing nack/sendNack/onNack
handler in this class) so the management server receives an explicit NACK rather
than silently ignoring the unknown type.
🧹 Nitpick comments (4)
xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java (1)

65-72: Reuse ListenerResourceParser for bootstrap listeners.

This is the same unpack/resolve sequence now implemented in ListenerResourceParser.parse(...), so the static-listener path can drift from the xDS parsing path.

Suggested refactor
-        final XdsExtensionRegistry registry = context.extensionRegistry();
-        final HttpConnectionManager connectionManager =
-                XdsUnpackUtil.unpackConnectionManager(listener, registry);
-        final ListenerXdsResource listenerResource =
-                new ListenerXdsResource(listener, connectionManager,
-                                        XdsUnpackUtil.resolveDownstreamFilters(connectionManager, registry),
-                                        "", 0);
+        final ListenerXdsResource listenerResource =
+                ListenerResourceParser.INSTANCE.parse(listener, context.extensionRegistry(), "", 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java` around lines
65 - 72, The static-listener path duplicates the unpack/resolve logic; replace
the manual calls to XdsUnpackUtil.unpackConnectionManager and
XdsUnpackUtil.resolveDownstreamFilters and the direct construction of
ListenerXdsResource in ListenerManager with a call to
ListenerResourceParser.parse(...) to produce the ListenerXdsResource, then pass
that resource into new ListenerStream(listenerResource, context); locate the
block that currently uses XdsExtensionRegistry registry,
XdsUnpackUtil.unpackConnectionManager(listener,...),
XdsUnpackUtil.resolveDownstreamFilters(...), and ListenerXdsResource(...) and
swap it to use ListenerResourceParser.parse(listener, registry, ...) (matching
the available parse signature) to ensure both paths share the same parsing
logic.
xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java (1)

82-95: Redundant condition in absent detection logic.

The condition on line 84-85 has redundancy:

  1. parser.isFullStateOfTheWorld() should already imply LDS or CDS types, making the explicit type check redundant
  2. The guard on line 87 !parsedResources.isEmpty() || !currentSubscribers.isEmpty() is unnecessary since an empty currentSubscribers set means the for-loop simply won't execute
♻️ Suggested simplification
         // SotW absent detection for full-state types (LDS/CDS)
         final ResourceParser<?, ?> parser = XdsResourceParserUtil.fromType(type);
-        if (parser != null && parser.isFullStateOfTheWorld() &&
-            (type == XdsType.LISTENER || type == XdsType.CLUSTER)) {
+        if (parser != null && parser.isFullStateOfTheWorld()) {
             final Set<String> currentSubscribers = stateCoordinator.interestedResources(type);
-            if (!parsedResources.isEmpty() || !currentSubscribers.isEmpty()) {
-                for (String name : currentSubscribers) {
-                    if (parsedResources.containsKey(name) || invalidResources.containsKey(name)) {
-                        continue;
-                    }
-                    stateCoordinator.onResourceMissing(type, name);
+            for (String name : currentSubscribers) {
+                if (parsedResources.containsKey(name) || invalidResources.containsKey(name)) {
+                    continue;
                 }
+                stateCoordinator.onResourceMissing(type, name);
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java` around
lines 82 - 95, The absent-detection logic in SubscriptionCallbacks currently
redundantly checks the XdsType and guards on parsedResources/currentSubscribers;
change the block to rely on XdsResourceParserUtil.fromType(type) and
parser.isFullStateOfTheWorld() only (remove the explicit type check for
XdsType.LISTENER/XdsType.CLUSTER) and drop the unnecessary if
(!parsedResources.isEmpty() || !currentSubscribers.isEmpty()) guard; keep
obtaining currentSubscribers via stateCoordinator.interestedResources(type) and
iterate it, calling stateCoordinator.onResourceMissing(type, name) for names not
present in parsedResources or invalidResources to preserve behavior.
xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java (1)

87-94: Consider logging or failing on duplicate factory registrations.

The register method silently overwrites existing entries if the same name or typeUrl is registered twice. This could hide configuration errors where two factories claim the same identifier.

♻️ Suggested improvement
 private static void register(XdsExtensionFactory factory,
                              Map<String, XdsExtensionFactory> byName,
                              Map<String, XdsExtensionFactory> byTypeUrl) {
-    byName.put(factory.name(), factory);
+    final XdsExtensionFactory prev = byName.put(factory.name(), factory);
+    if (prev != null) {
+        logger.warn("Duplicate factory registration for name '{}': {} replaced by {}",
+                    factory.name(), prev.getClass().getName(), factory.getClass().getName());
+    }
     for (String typeUrl : factory.typeUrls()) {
-        byTypeUrl.put(typeUrl, factory);
+        final XdsExtensionFactory prevUrl = byTypeUrl.put(typeUrl, factory);
+        if (prevUrl != null) {
+            logger.warn("Duplicate factory registration for typeUrl '{}': {} replaced by {}",
+                        typeUrl, prevUrl.getClass().getName(), factory.getClass().getName());
+        }
     }
 }

This would require adding a logger field:

private static final Logger logger = LoggerFactory.getLogger(XdsExtensionRegistry.class);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java` around
lines 87 - 94, The register method currently overwrites existing entries; add a
private static final Logger
(LoggerFactory.getLogger(XdsExtensionRegistry.class)) and in register check
byName.containsKey(factory.name()) and byTypeUrl.containsKey(typeUrl) before
putting; when a duplicate is detected log a clear warning including the
conflicting name/typeUrl and both factory identifiers and then fail fast by
throwing an IllegalStateException to surface configuration errors instead of
silently overwriting; reference the register method, XdsExtensionFactory.name(),
XdsExtensionFactory.typeUrls(), and the byName/byTypeUrl maps when implementing
the checks.
xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto (1)

338-341: Document canonical IPv6 format for IP_ADDRESS SAN matching.

Since IP_ADDRESS is now explicitly supported, add a brief proto note that IPv6 SANs should use canonical lowercase compressed notation to prevent unexpected mismatch behavior.

Suggested doc-only patch
   enum SanType {
     SAN_TYPE_UNSPECIFIED = 0;
     EMAIL = 1 [(armeria.xds.supported_value) = true];
     DNS = 2 [(armeria.xds.supported_value) = true];
     URI = 3 [(armeria.xds.supported_value) = true];
+    // For IPv6, use canonical lowercase compressed form (e.g. "2001:db8::1")
+    // to match runtime SAN normalization behavior.
     IP_ADDRESS = 4 [(armeria.xds.supported_value) = true];
     OTHER_NAME = 5;
   }

Based on learnings: In SanMatcher.java, IPv6 SAN matching intentionally uses canonical InetAddress.getHostAddress() form, requiring canonical lowercase compressed notation in xDS config.

Also applies to: 346-347, 364-365

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto`
around lines 338 - 341, Add a proto-side note documenting the canonical IPv6
format required for IP_ADDRESS SAN matching: update the enum block containing
EMAIL, DNS, URI, IP_ADDRESS to include a brief comment stating that IPv6 SANs
must use canonical lowercase compressed notation (the same form returned by
InetAddress.getHostAddress()) to avoid mismatches with SanMatcher.java;
reference the IP_ADDRESS enum symbol and mention the behavior in SanMatcher.java
that performs matching using InetAddress.getHostAddress() so config authors know
to supply canonical IPv6 addresses.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java`:
- Around line 46-49: The public API DefaultXdsValidatorIndex.assertValid
currently passes a null through to pgvValidator; add an explicit null-safety
check at the start of assertValid(Object message) to reject null inputs (throw a
NullPointerException with a clear message) before calling
pgvValidator.assertValid(message) and
supportedFieldValidator.assertValid(message), so that the method fails fast and
documents the contract at the API boundary.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java`:
- Around line 46-60: The javadoc describes a message-level option
(armeria.xds.supported_fields) but SupportedFieldValidator.supportedFields(...)
only reads per-field (supported) annotations, so message-level declared
supported field numbers must be read too; update supportedFields(...) in
SupportedFieldValidator to also inspect the message descriptor's options for the
repeated int32 armeria.xds.supported_fields extension (merge its values with any
per-field supported flags), treating those numbers as supported field indices
and preventing them from being reported unsupported; ensure you correctly access
the message options extension (the generated extension identifier for
armeria.xds.supported_fields) and handle duplicates and out-of-range numbers
consistently with existing validation logic.
- Around line 101-105: The public method SupportedFieldValidator.assertValid
currently silently ignores a null argument; add an explicit null-check at the
start (e.g., Objects.requireNonNull or equivalent) to throw a
NullPointerException for null inputs so callers fail fast and match
validate(Message) expectations, then continue to cast and call
validate((Message) message) as before.

In `@xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java`:
- Around line 49-56: The constructor stores downstreamFilters by reference then
computes router from it, so subsequent mutations to the stored list can make
router stale and withRevision reuses the same mutable list; to fix, create an
immutable/defensive copy of downstreamFilters (e.g., via
Collections.unmodifiableList or List.copyOf) as the stored field before calling
findRouter(...) and ensure withRevision(...) also passes that immutable copy (or
creates a fresh immutable copy) so router is always derived from an immutable
list; update the ListenerXdsResource constructor, the downstreamFilters field
initialization, and withRevision() to use the frozen list and compute router
from that frozen list via findRouter.

In `@xds/src/main/java/com/linecorp/armeria/xds/StateCoordinator.java`:
- Around line 66-69: The replay logic currently calls the watcher directly
(inside register -> replayToWatcher), which bypasses the XdsStreamSubscriber
lifecycle and leaves its initial-absent timeout active; update replayToWatcher
(and the other similar replay spot) to invoke the subscriber's handlers instead
of calling the watcher directly—specifically call
XdsStreamSubscriber.onData(...) when delivering a VERSIONED state and
XdsStreamSubscriber.onAbsent(...) when delivering an ABSENT state—or, if you
prefer to keep watcher invocation, cancel the subscriber's initial-absent
timeout before delivering cached state so the subscriber never later fires
onAbsent; apply the same change to the other replay code path referenced in this
file to ensure timeouts are not left running.

---

Duplicate comments:
In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java`:
- Around line 19-20: StrictXdsValidatorIndex's public API lacks a null-guard:
add an explicit null check (e.g., Objects.requireNonNull(...)) at the start of
the public assertValid(...) method(s) in class StrictXdsValidatorIndex so
assertValid(null) throws immediately rather than delegating to downstream
validators; place the requireNonNull call at the top of the public method(s)
referenced (assertValid) to enforce null-safety for the API boundary.

In
`@xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java`:
- Around line 97-103: The code only validates grpc_services[0]; instead iterate
apiConfigSource.getGrpcServicesList() and assert each GrpcService.hasEnvoyGrpc()
(e.g., for (GrpcService gs : grpcServices) checkArgument(gs.hasEnvoyGrpc(),
"Only envoyGrpc is supported for '%s'", configSource)); then keep the existing
firstGrpcService/getEnvoyGrpc() usage to obtain envoyGrpc after the per-entry
validation. This ensures every configured GrpcService is validated, not just the
first.

In `@xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java`:
- Around line 153-172: The onError/onCompleted paths must ignore terminal
callbacks if closeStream() already marked the stream completed; add an early
guard in both onError(Throwable) and onCompleted() that returns immediately when
completed is true (before scheduling onto eventLoop) so you don't reschedule
work or call lifecycleObserver.streamError/streamCompleted or
owner.retryOrClose(...) after a local close; reference the existing completed
flag, the onError/onCompleted methods, lifecycleObserver, owner.retryOrClose,
and closeStream() when making this change.
- Around line 145-149: The code in SotwActualStream currently logs and returns
when XdsResourceParserUtil.fromTypeUrl(value.getTypeUrl()) returns null, which
drops the response; instead construct and send a NACK for this
DiscoveryResponse: build an error Status (e.g., INVALID_ARGUMENT) with a
descriptive message including value.getTypeUrl(), then call the class's existing
NACK pathway using the DiscoveryResponse identifiers (value.getVersionInfo(),
value.getNonce(), and optionally resource names from value.getResourcesList()) —
reuse the SotwActualStream method that sends NACKs (the existing
nack/sendNack/onNack handler in this class) so the management server receives an
explicit NACK rather than silently ignoring the unknown type.

---

Nitpick comments:
In
`@xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto`:
- Around line 338-341: Add a proto-side note documenting the canonical IPv6
format required for IP_ADDRESS SAN matching: update the enum block containing
EMAIL, DNS, URI, IP_ADDRESS to include a brief comment stating that IPv6 SANs
must use canonical lowercase compressed notation (the same form returned by
InetAddress.getHostAddress()) to avoid mismatches with SanMatcher.java;
reference the IP_ADDRESS enum symbol and mention the behavior in SanMatcher.java
that performs matching using InetAddress.getHostAddress() so config authors know
to supply canonical IPv6 addresses.

In `@xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java`:
- Around line 65-72: The static-listener path duplicates the unpack/resolve
logic; replace the manual calls to XdsUnpackUtil.unpackConnectionManager and
XdsUnpackUtil.resolveDownstreamFilters and the direct construction of
ListenerXdsResource in ListenerManager with a call to
ListenerResourceParser.parse(...) to produce the ListenerXdsResource, then pass
that resource into new ListenerStream(listenerResource, context); locate the
block that currently uses XdsExtensionRegistry registry,
XdsUnpackUtil.unpackConnectionManager(listener,...),
XdsUnpackUtil.resolveDownstreamFilters(...), and ListenerXdsResource(...) and
swap it to use ListenerResourceParser.parse(listener, registry, ...) (matching
the available parse signature) to ensure both paths share the same parsing
logic.

In `@xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java`:
- Around line 82-95: The absent-detection logic in SubscriptionCallbacks
currently redundantly checks the XdsType and guards on
parsedResources/currentSubscribers; change the block to rely on
XdsResourceParserUtil.fromType(type) and parser.isFullStateOfTheWorld() only
(remove the explicit type check for XdsType.LISTENER/XdsType.CLUSTER) and drop
the unnecessary if (!parsedResources.isEmpty() || !currentSubscribers.isEmpty())
guard; keep obtaining currentSubscribers via
stateCoordinator.interestedResources(type) and iterate it, calling
stateCoordinator.onResourceMissing(type, name) for names not present in
parsedResources or invalidResources to preserve behavior.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java`:
- Around line 87-94: The register method currently overwrites existing entries;
add a private static final Logger
(LoggerFactory.getLogger(XdsExtensionRegistry.class)) and in register check
byName.containsKey(factory.name()) and byTypeUrl.containsKey(typeUrl) before
putting; when a duplicate is detected log a clear warning including the
conflicting name/typeUrl and both factory identifiers and then fail fast by
throwing an IllegalStateException to surface configuration errors instead of
silently overwriting; reference the register method, XdsExtensionFactory.name(),
XdsExtensionFactory.typeUrls(), and the byName/byTypeUrl maps when implementing
the checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7a43eb57-43a0-41aa-bcbd-4c28685d5027

📥 Commits

Reviewing files that changed from the base of the PR and between 2631fa8 and 1e93d34.

📒 Files selected for processing (89)
  • it/xds-client/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/IgnoreUnsupportedFieldHandler.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandler.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/UnsupportedFieldHandler.java
  • xds-api/src/main/proto/armeria/xds/supported.proto
  • xds-api/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto
  • xds-api/src/main/proto/envoy/config/cluster/v3/cluster.proto
  • xds-api/src/main/proto/envoy/config/core/v3/address.proto
  • xds-api/src/main/proto/envoy/config/core/v3/base.proto
  • xds-api/src/main/proto/envoy/config/core/v3/config_source.proto
  • xds-api/src/main/proto/envoy/config/core/v3/grpc_service.proto
  • xds-api/src/main/proto/envoy/config/core/v3/health_check.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint_components.proto
  • xds-api/src/main/proto/envoy/config/listener/v3/api_listener.proto
  • xds-api/src/main/proto/envoy/config/listener/v3/listener.proto
  • xds-api/src/main/proto/envoy/config/route/v3/route.proto
  • xds-api/src/main/proto/envoy/config/route/v3/route_components.proto
  • xds-api/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto
  • xds-api/src/main/proto/envoy/type/matcher/v3/regex.proto
  • xds-api/src/main/proto/envoy/type/matcher/v3/string.proto
  • xds-api/src/main/proto/envoy/type/v3/percent.proto
  • xds-api/src/main/proto/envoy/type/v3/range.proto
  • xds-api/src/test/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandlerTest.java
  • xds-api/src/test/java/com/linecorp/armeria/xds/api/SupportedFieldValidatorTest.java
  • xds-api/src/test/proto/armeria/xds/testing/test_supported.proto
  • xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceClient.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceSubscription.java
  • xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/DefaultSubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/HttpConnectionManagerFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/RawBufferTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceNodeAdapter.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwSubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/StateCoordinator.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/UpstreamTlsTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/VirtualHostXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapImpl.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsClusterManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsConverterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsClientCleanupTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsResourceValidatorTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistryTest.java
  • xds/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
💤 Files with no reviewable changes (5)
  • xds/src/test/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistryTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsConverterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceClient.java
✅ Files skipped from review due to trivial changes (17)
  • it/xds-client/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • xds/src/main/java/com/linecorp/armeria/xds/XdsClusterManager.java
  • xds/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwSubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceSubscription.java
  • xds-api/src/test/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandlerTest.java
  • xds-api/src/main/proto/armeria/xds/supported.proto
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java
  • xds-api/src/main/proto/envoy/type/v3/percent.proto
  • xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java
  • xds-api/src/main/proto/envoy/config/route/v3/route.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint.proto
  • xds-api/src/test/proto/armeria/xds/testing/test_supported.proto
  • xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java
🚧 Files skipped from review as they are similar to previous changes (34)
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceNodeAdapter.java
  • xds-api/src/main/proto/envoy/config/listener/v3/api_listener.proto
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/HttpConnectionManagerFactory.java
  • xds-api/src/main/proto/envoy/type/v3/range.proto
  • xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapImpl.java
  • xds/src/main/java/com/linecorp/armeria/xds/RawBufferTransportSocketFactory.java
  • xds-api/src/main/proto/envoy/config/listener/v3/listener.proto
  • xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsResourceValidatorTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/UnsupportedFieldHandler.java
  • xds-api/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto
  • xds-api/src/main/proto/envoy/type/matcher/v3/string.proto
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/VirtualHostXdsResource.java
  • xds-api/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto
  • xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java
  • xds-api/src/main/proto/envoy/config/core/v3/base.proto
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointXdsResource.java
  • xds-api/src/main/proto/envoy/config/core/v3/health_check.proto
  • xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto

Comment thread xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java Outdated
Comment on lines +46 to +60
* <p>If inline annotations cause frequent merge conflicts when syncing upstream proto files,
* an alternative is to use a message-level option listing supported field numbers:
* <pre>{@code
* extend google.protobuf.MessageOptions {
* repeated int32 supported_fields = 50000;
* }
*
* message StringMatcher {
* option (armeria.xds.supported_fields) = 1; // exact
* option (armeria.xds.supported_fields) = 2; // prefix
* string exact = 1;
* string prefix = 2 [(validate.rules).string = {min_len: 1}];
* }
* }</pre>
* This keeps upstream field declarations untouched, at the cost of reduced readability.
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.

⚠️ Potential issue | 🟡 Minor

supported_fields is documented but never consulted.

The Javadoc advertises a message-level supported_fields option, but supportedFields(...) only inspects per-field (supported) options. Any proto that follows the documented message-option pattern will still be reported unsupported.

Also applies to: 209-218

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java`
around lines 46 - 60, The javadoc describes a message-level option
(armeria.xds.supported_fields) but SupportedFieldValidator.supportedFields(...)
only reads per-field (supported) annotations, so message-level declared
supported field numbers must be read too; update supportedFields(...) in
SupportedFieldValidator to also inspect the message descriptor's options for the
repeated int32 armeria.xds.supported_fields extension (merge its values with any
per-field supported flags), treating those numbers as supported field indices
and preventing them from being reported unsupported; ensure you correctly access
the message options extension (the generated extension identifier for
armeria.xds.supported_fields) and handle duplicates and out-of-range numbers
consistently with existing validation logic.

Comment thread xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java Outdated
Comment on lines +49 to +56
ListenerXdsResource(Listener listener, @Nullable HttpConnectionManager connectionManager,
List<XdsHttpFilter> downstreamFilters,
String version, long revision) {
super(version, revision);
XdsValidatorIndexRegistry.assertValid(listener);
this.listener = listener;
this.connectionManager = connectionManager;
this.downstreamFilters = downstreamFilters;
this.router = findRouter(downstreamFilters);
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.

⚠️ Potential issue | 🟡 Minor

Freeze downstreamFilters before deriving router.

router is computed once, but downstreamFilters is stored and exposed by reference. A later mutation makes router() stale relative to downstreamFilters(), and withRevision() keeps reusing the same mutable list.

Proposed fix
-        this.downstreamFilters = downstreamFilters;
-        this.router = findRouter(downstreamFilters);
+        this.downstreamFilters = ImmutableList.copyOf(downstreamFilters);
+        this.router = findRouter(this.downstreamFilters);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ListenerXdsResource(Listener listener, @Nullable HttpConnectionManager connectionManager,
List<XdsHttpFilter> downstreamFilters,
String version, long revision) {
super(version, revision);
XdsValidatorIndexRegistry.assertValid(listener);
this.listener = listener;
this.connectionManager = connectionManager;
this.downstreamFilters = downstreamFilters;
this.router = findRouter(downstreamFilters);
ListenerXdsResource(Listener listener, `@Nullable` HttpConnectionManager connectionManager,
List<XdsHttpFilter> downstreamFilters,
String version, long revision) {
super(version, revision);
this.listener = listener;
this.connectionManager = connectionManager;
this.downstreamFilters = ImmutableList.copyOf(downstreamFilters);
this.router = findRouter(this.downstreamFilters);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java` around
lines 49 - 56, The constructor stores downstreamFilters by reference then
computes router from it, so subsequent mutations to the stored list can make
router stale and withRevision reuses the same mutable list; to fix, create an
immutable/defensive copy of downstreamFilters (e.g., via
Collections.unmodifiableList or List.copyOf) as the stored field before calling
findRouter(...) and ensure withRevision(...) also passes that immutable copy (or
creates a fresh immutable copy) so router is always derived from an immutable
list; update the ListenerXdsResource constructor, the downstreamFilters field
initialization, and withRevision() to use the frozen list and compute router
from that frozen list via findRouter.

Comment on lines +66 to +69
<T extends XdsResource> boolean register(XdsType type, String resourceName, ResourceWatcher<T> watcher) {
final boolean updated = subscriberStorage.register(type, resourceName, watcher);
replayToWatcher(type, resourceName, watcher);
return updated;
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== SubscriberStorage register/unregister =="
fd -p 'SubscriberStorage.java' | while read -r file; do
  echo "-- $file --"
  rg -n -C4 'register\(|unregister\(' "$file"
done

echo
echo "== XdsStreamSubscriber lifecycle =="
fd -p 'XdsStreamSubscriber.java' | while read -r file; do
  echo "-- $file --"
  rg -n -C4 'class XdsStreamSubscriber|registerWatcher\(|onData\(|onAbsent\(|onError\(|timeout|schedule' "$file"
done

Repository: line/armeria

Length of output: 4000


🏁 Script executed:

fd -p 'StateCoordinator.java' | head -1 | xargs wc -l

Repository: line/armeria

Length of output: 124


🏁 Script executed:

fd -p 'StateCoordinator.java' | head -1 | xargs cat -n | sed -n '1,200p'

Repository: line/armeria

Length of output: 7150


🏁 Script executed:

fd -p 'StateCoordinator.java' | head -1 | xargs rg -n 'replayToWatcher|register\(' -A 10 -B 2

Repository: line/armeria

Length of output: 1631


Replay paths should invoke subscriber state methods, not watcher directly.

When register() creates a fresh XdsStreamSubscriber, it unconditionally schedules an initial-absent timeout in the constructor. However, replayToWatcher() immediately delivers cached VERSIONED or ABSENT state directly to the watcher, bypassing the subscriber's onData()/onAbsent() methods. This leaves the timeout running, so if the server never sends a new update, the timeout fires and invokes subscriber.onAbsent(), causing the watcher to receive an unexpected onResourceDoesNotExist() notification after already receiving a cached onChanged() or onResourceDoesNotExist().

Replay should either call subscriber.onData() / subscriber.onAbsent() instead of invoking the watcher directly, or cancel the timeout when cached state is delivered.

Also applies to: 135-153

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/StateCoordinator.java` around
lines 66 - 69, The replay logic currently calls the watcher directly (inside
register -> replayToWatcher), which bypasses the XdsStreamSubscriber lifecycle
and leaves its initial-absent timeout active; update replayToWatcher (and the
other similar replay spot) to invoke the subscriber's handlers instead of
calling the watcher directly—specifically call XdsStreamSubscriber.onData(...)
when delivering a VERSIONED state and XdsStreamSubscriber.onAbsent(...) when
delivering an ABSENT state—or, if you prefer to keep watcher invocation, cancel
the subscriber's initial-absent timeout before delivering cached state so the
subscriber never later fires onAbsent; apply the same change to the other replay
code path referenced in this file to ensure timeouts are not left running.

@jrhee17 jrhee17 force-pushed the feat/xds-support-field branch from 1e93d34 to 953fc02 Compare April 20, 2026 06:42
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java (1)

38-46: Consider caching the SupportedFieldValidator.

PgvValidator is cached as an instance field, but a fresh SupportedFieldValidator (plus a new ArrayList and lambda) is allocated on every call. SupportedFieldValidator keeps a ConcurrentHashMap cache for supportedFields(descriptor) (per the shown snippet in SupportedFieldValidator.java:209-219) — that cache is thrown away each call since the instance is new. Passing a per-call collector via a method parameter (or a thread-local list) would preserve the descriptor cache across invocations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java`
around lines 38 - 46, StrictXdsValidatorIndex.assertValid creates a new
SupportedFieldValidator (and a new ArrayList and lambda) on every call which
discards SupportedFieldValidator's internal descriptor cache; make
SupportedFieldValidator an instance field (like pgvValidator) so its
ConcurrentHashMap cache is reused across invocations, and change its API or use
an assertValid(message, Consumer<String>) overload (or pass the per-call
violations list into a method) so you can supply the per-call violations
collector without recreating the validator each time.
xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java (2)

81-123: Consider folding resolveStreamFactory back into createClient.

createClient switches on configSourceSpecifierCase, and for the SotW cases immediately calls resolveStreamFactory, which re-switches on the same case. The indirection costs an extra traversal and splits the SotW handling between two methods for no clear benefit — there's no second caller of resolveStreamFactory. Inlining the two queryBy… calls into the two case bodies is more direct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java`
around lines 81 - 123, The helper method resolveStreamFactory duplicates the
switch on configSourceSpecifierCase called immediately from createClient,
splitting SotW handling across two methods; inline its logic into createClient
by replacing the call to resolveStreamFactory(configSource) with direct queries
on extensionRegistry: for PATH_CONFIG_SOURCE call
extensionRegistry.queryByName(PathSotwConfigSourceSubscriptionFactory.NAME,
SotwConfigSourceSubscriptionFactory.class) and for CUSTOM_CONFIG_SOURCE call
extensionRegistry.queryByTypeUrl(configSource.getCustomConfigSource().getTypeUrl(),
SotwConfigSourceSubscriptionFactory.class), keep the existing checkArgument and
use the resulting SotwConfigSourceSubscriptionFactory as before, then remove the
resolveStreamFactory method.

88-89: Nit: magic boolean at StateCoordinator(..., false).

The trailing false has no call-site context; a named constant or a small comment (/* delta= */ false, or whatever it represents) would make this line self-explanatory and harder to misuse during future refactors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java`
around lines 88 - 89, The call to new StateCoordinator(eventLoop, configSource,
false) uses a magic boolean; replace the literal with a named constant or add an
inline parameter comment so the intent is explicit. For example, define a
private static final boolean INITIAL_DELTA = false and pass that, or annotate
the call like /* delta= */ false, referencing the StateCoordinator constructor
so future readers and refactors know what the flag means.
xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java (2)

87-94: Detect duplicate factory registrations rather than silently overwriting.

Two SPI-discovered factories (or an SPI factory colliding with a built-in) that share a name() or the same typeUrls() entry will silently clobber each other through Map.put. For an extension registry loaded via ServiceLoader, this is a class of misconfiguration that should fail loudly.

♻️ Suggested change
     private static void register(XdsExtensionFactory factory,
                                  Map<String, XdsExtensionFactory> byName,
                                  Map<String, XdsExtensionFactory> byTypeUrl) {
-        byName.put(factory.name(), factory);
-        for (String typeUrl : factory.typeUrls()) {
-            byTypeUrl.put(typeUrl, factory);
-        }
+        final XdsExtensionFactory prevByName = byName.putIfAbsent(factory.name(), factory);
+        checkArgument(prevByName == null || prevByName == factory,
+                      "Duplicate factory name '%s': %s vs %s",
+                      factory.name(), prevByName, factory);
+        for (String typeUrl : factory.typeUrls()) {
+            final XdsExtensionFactory prev = byTypeUrl.putIfAbsent(typeUrl, factory);
+            checkArgument(prev == null || prev == factory,
+                          "Duplicate factory typeUrl '%s': %s vs %s", typeUrl, prev, factory);
+        }
     }

As per coding guidelines: "Use Guava Preconditions.checkArgument/checkState where it simplifies argument/state validation."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java` around
lines 87 - 94, The register method currently overwrites existing entries; update
XdsExtensionRegistry.register(XdsExtensionFactory factory, Map<String,
XdsExtensionFactory> byName, Map<String, XdsExtensionFactory> byTypeUrl) to
detect duplicates and fail fast: before byName.put(...) use Guava
Preconditions.checkState(!byName.containsKey(factory.name()), "Duplicate factory
name: %s (already registered by %s)", factory.name(),
byName.get(factory.name())); and for each typeUrl in factory.typeUrls() use
Preconditions.checkState(!byTypeUrl.containsKey(typeUrl), "Duplicate typeUrl: %s
(already registered by %s)", typeUrl, byTypeUrl.get(typeUrl)); only then insert
into the maps; include the existing conflicting factory in the error message to
aid debugging.

161-171: Nit: document the "wrong type at typeUrl short-circuits name fallback" behavior.

If a factory is registered at any.getTypeUrl() but does not implement expectedType, queryByTypeUrl throws IAE and query never falls through to queryByName. That may be intentional (fail fast on a confused typeUrl), but given the method's contract advertises "resolves by typeUrl first, then by name", callers may reasonably expect the name fallback to still execute. Consider either catching the type mismatch and trying name, or adding a line to the Javadoc making the short-circuit explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java` around
lines 161 - 171, The query method currently short-circuits when
queryByTypeUrl(any.getTypeUrl(), expectedType) throws an
IllegalArgumentException for a type mismatch, preventing the name fallback;
update query(Any any, String name, Class<T> expectedType) to catch the specific
type-mismatch IAE from queryByTypeUrl and, on that case, continue to call
queryByName(name, expectedType) if name is non-null (rethrow other unexpected
exceptions), so callers still get name-based resolution; alternatively, if you
prefer not to change runtime behavior, add Javadoc to query explaining that a
typeUrl-present-but-wrong-type will abort and prevent name fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java`:
- Around line 28-31: The parse() method in ClusterResourceParser no longer
performs EDS-specific config_source validation and relies only on
registry.assertValid(); restore the original EDS validation by calling
XdsConverterUtil.validateConfigSource(...) on the Cluster's eds.getEdsConfig()
inside ClusterResourceParser.parse before constructing the ClusterXdsResource
(so use XdsConverterUtil.validateConfigSource(eds.getEdsConfig()) and, if it
indicates invalid, throw or surface an XdsResourceException consistent with
previous behavior). This keeps fast-fail parsing while leaving ClusterStream's
runtime configSourceMapper.configSource() check as an additional safeguard.

In `@xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java`:
- Around line 81-107: The exception messages in createClient currently
interpolate the full ConfigSource proto (in the checkArgument and
IllegalArgumentException); instead, change both messages to avoid printing the
entire proto by including only configSource.getConfigSourceSpecifierCase() and,
when the case is CUSTOM_CONFIG_SOURCE, also include
configSource.getCustomConfigSource().getTypeUrl() (or null-safe equivalent) to
identify the offending value; update the checkArgument call that references "No
SotwConfigSourceSubscriptionFactory found for: %s" and the
IllegalArgumentException that concatenates configSource so they use these safe,
minimal identifiers (reference: createClient, resolveStreamFactory,
ConfigSourceHandler, and the ConfigSourceSpecifierCase getters).

---

Nitpick comments:
In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java`:
- Around line 38-46: StrictXdsValidatorIndex.assertValid creates a new
SupportedFieldValidator (and a new ArrayList and lambda) on every call which
discards SupportedFieldValidator's internal descriptor cache; make
SupportedFieldValidator an instance field (like pgvValidator) so its
ConcurrentHashMap cache is reused across invocations, and change its API or use
an assertValid(message, Consumer<String>) overload (or pass the per-call
violations list into a method) so you can supply the per-call violations
collector without recreating the validator each time.

In `@xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java`:
- Around line 81-123: The helper method resolveStreamFactory duplicates the
switch on configSourceSpecifierCase called immediately from createClient,
splitting SotW handling across two methods; inline its logic into createClient
by replacing the call to resolveStreamFactory(configSource) with direct queries
on extensionRegistry: for PATH_CONFIG_SOURCE call
extensionRegistry.queryByName(PathSotwConfigSourceSubscriptionFactory.NAME,
SotwConfigSourceSubscriptionFactory.class) and for CUSTOM_CONFIG_SOURCE call
extensionRegistry.queryByTypeUrl(configSource.getCustomConfigSource().getTypeUrl(),
SotwConfigSourceSubscriptionFactory.class), keep the existing checkArgument and
use the resulting SotwConfigSourceSubscriptionFactory as before, then remove the
resolveStreamFactory method.
- Around line 88-89: The call to new StateCoordinator(eventLoop, configSource,
false) uses a magic boolean; replace the literal with a named constant or add an
inline parameter comment so the intent is explicit. For example, define a
private static final boolean INITIAL_DELTA = false and pass that, or annotate
the call like /* delta= */ false, referencing the StateCoordinator constructor
so future readers and refactors know what the flag means.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java`:
- Around line 87-94: The register method currently overwrites existing entries;
update XdsExtensionRegistry.register(XdsExtensionFactory factory, Map<String,
XdsExtensionFactory> byName, Map<String, XdsExtensionFactory> byTypeUrl) to
detect duplicates and fail fast: before byName.put(...) use Guava
Preconditions.checkState(!byName.containsKey(factory.name()), "Duplicate factory
name: %s (already registered by %s)", factory.name(),
byName.get(factory.name())); and for each typeUrl in factory.typeUrls() use
Preconditions.checkState(!byTypeUrl.containsKey(typeUrl), "Duplicate typeUrl: %s
(already registered by %s)", typeUrl, byTypeUrl.get(typeUrl)); only then insert
into the maps; include the existing conflicting factory in the error message to
aid debugging.
- Around line 161-171: The query method currently short-circuits when
queryByTypeUrl(any.getTypeUrl(), expectedType) throws an
IllegalArgumentException for a type mismatch, preventing the name fallback;
update query(Any any, String name, Class<T> expectedType) to catch the specific
type-mismatch IAE from queryByTypeUrl and, on that case, continue to call
queryByName(name, expectedType) if name is non-null (rethrow other unexpected
exceptions), so callers still get name-based resolution; alternatively, if you
prefer not to change runtime behavior, add Javadoc to query explaining that a
typeUrl-present-but-wrong-type will abort and prevent name fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aaf1521a-1bca-4573-9b91-767039c3d2b1

📥 Commits

Reviewing files that changed from the base of the PR and between 1e93d34 and 953fc02.

📒 Files selected for processing (88)
  • it/xds-client/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/IgnoreUnsupportedFieldHandler.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandler.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/SupportedFieldValidator.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/UnsupportedFieldHandler.java
  • xds-api/src/main/proto/armeria/xds/supported.proto
  • xds-api/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto
  • xds-api/src/main/proto/envoy/config/cluster/v3/cluster.proto
  • xds-api/src/main/proto/envoy/config/core/v3/address.proto
  • xds-api/src/main/proto/envoy/config/core/v3/base.proto
  • xds-api/src/main/proto/envoy/config/core/v3/config_source.proto
  • xds-api/src/main/proto/envoy/config/core/v3/grpc_service.proto
  • xds-api/src/main/proto/envoy/config/core/v3/health_check.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint_components.proto
  • xds-api/src/main/proto/envoy/config/listener/v3/api_listener.proto
  • xds-api/src/main/proto/envoy/config/listener/v3/listener.proto
  • xds-api/src/main/proto/envoy/config/route/v3/route.proto
  • xds-api/src/main/proto/envoy/config/route/v3/route_components.proto
  • xds-api/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto
  • xds-api/src/main/proto/envoy/type/matcher/v3/regex.proto
  • xds-api/src/main/proto/envoy/type/matcher/v3/string.proto
  • xds-api/src/main/proto/envoy/type/v3/percent.proto
  • xds-api/src/main/proto/envoy/type/v3/range.proto
  • xds-api/src/test/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandlerTest.java
  • xds-api/src/test/java/com/linecorp/armeria/xds/api/SupportedFieldValidatorTest.java
  • xds-api/src/test/proto/armeria/xds/testing/test_supported.proto
  • xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceClient.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceSubscription.java
  • xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/DefaultSubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/HttpConnectionManagerFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/RawBufferTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceNodeAdapter.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwSubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/StateCoordinator.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/UpstreamTlsTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/VirtualHostXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapImpl.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsClusterManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsConverterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsClientCleanupTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsResourceValidatorTest.java
  • xds/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
💤 Files with no reviewable changes (9)
  • xds/src/main/java/com/linecorp/armeria/xds/SecretXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/VirtualHostXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsConverterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceClient.java
✅ Files skipped from review due to trivial changes (21)
  • it/xds-client/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
  • xds/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • xds-api/src/test/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandlerTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketFactory.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto
  • xds-api/src/main/proto/envoy/type/v3/range.proto
  • xds-api/src/main/proto/armeria/xds/supported.proto
  • xds/src/main/java/com/linecorp/armeria/xds/HttpConnectionManagerFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsClusterManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/RawBufferTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwSubscriptionCallbacks.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/UnsupportedFieldHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionFactory.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsResourceValidatorTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java
  • xds-api/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto
🚧 Files skipped from review as they are similar to previous changes (28)
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionContext.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/IgnoreUnsupportedFieldHandler.java
  • xds-api/src/main/proto/envoy/config/listener/v3/api_listener.proto
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java
  • xds-api/src/main/proto/envoy/config/listener/v3/listener.proto
  • xds/src/main/java/com/linecorp/armeria/xds/SotwConfigSourceSubscriptionFactory.java
  • xds-api/src/test/java/com/linecorp/armeria/xds/api/SupportedFieldValidatorTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
  • xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint.proto
  • xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapImpl.java
  • xds-api/src/main/proto/envoy/config/core/v3/address.proto
  • xds/src/main/java/com/linecorp/armeria/xds/SecretResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java
  • xds-api/src/main/proto/envoy/config/route/v3/route.proto
  • xds/src/main/java/com/linecorp/armeria/xds/DefaultSubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/UpstreamTlsTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java
  • xds-api/src/main/proto/envoy/type/v3/percent.proto
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/MeterUnsupportedFieldHandler.java
  • xds-api/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto
  • xds/src/main/java/com/linecorp/armeria/xds/StateCoordinator.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java
  • xds-api/src/main/proto/envoy/config/core/v3/health_check.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto

Comment on lines 28 to 31
ClusterXdsResource parse(Cluster cluster, XdsExtensionRegistry registry,
String version, long revision) {
return new ClusterXdsResource(cluster, version);
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where EDS config_source is now validated (if at all)
rg -nP --type=java -C3 '\bgetEdsConfig\s*\(\s*\)'
rg -nP --type=java -C3 'validateConfigSource'
rg -nP --type=java -C3 'EdsClusterConfig'

Repository: line/armeria

Length of output: 8862


🏁 Script executed:

# Find ConfigSourceMapper and its configSource method
rg -nP --type=java 'class ConfigSourceMapper|\.configSource\(' | head -40
# Search for validateConfigSource implementation
rg -nP --type=java 'validateConfigSource|assertValid' | head -40

Repository: line/armeria

Length of output: 4929


🏁 Script executed:

# Find XdsConverterUtil to understand old validation
rg -nP --type=java 'class XdsConverterUtil' -A 20
# Look for where ClusterXdsResource is instantiated/used
rg -nP --type=java 'new ClusterXdsResource|ClusterXdsResource.*=' | head -20

Repository: line/armeria

Length of output: 4075


🏁 Script executed:

# Check the registry's assertValid method
rg -nP --type=java 'assertValid.*\{' -A 10

Repository: line/armeria

Length of output: 11521


🏁 Script executed:

# Read ConfigSourceMapper implementation
cat xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceMapper.java

Repository: line/armeria

Length of output: 2297


🏁 Script executed:

# Check ResourceParser to see when assertValid is called
cat -n xds/src/main/java/com/linecorp/armeria/xds/ResourceParser.java | head -100

Repository: line/armeria

Length of output: 4860


🏁 Script executed:

# Search for any remaining EDS config_source validation
rg -nP --type=java 'getEdsConfig|EdsClusterConfig' -B2 -A2 | grep -A5 -B5 'validate\|null'

Repository: line/armeria

Length of output: 861


🏁 Script executed:

# Search for old validateConfigSource implementation or tests that show what it did
rg -nP --type=java 'validateConfigSource' -B5 -A10

Repository: line/armeria

Length of output: 38


🏁 Script executed:

# Check git history or find any comments about EDS validation
rg -nP --type=java 'EDS|eds' xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java -B3 -A3

Repository: line/armeria

Length of output: 38


🏁 Script executed:

# Get full context around the configSource null check in ClusterStream
sed -n '238,260p' xds/src/main/java/com/linecorp/armeria/xds/ClusterStream.java

Repository: line/armeria

Length of output: 1518


🏁 Script executed:

# Check ClusterResourceParser file to confirm it doesn't validate EDS
cat -n xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java

Repository: line/armeria

Length of output: 1895


The EDS config_source validation check has moved to downstream usage, not removed entirely.

The previous parse() method performed explicit XdsConverterUtil.validateConfigSource(eds.getEdsConfig()) during parsing. This is now gone from ClusterResourceParser.parse(), and the generic registry.assertValid() (which only runs PGV and supported-field checks) does not substitute for that EDS-specific validation.

However, the validation is not completely absent. In ClusterStream (lines 249–253), when the ClusterXdsResource is actually used, configSourceMapper.configSource() is called on the EDS config, and if it returns null (indicating an unresolvable config source), an error is immediately thrown:

if (configSource == null) {
    final SnapshotStream<Optional<EndpointSnapshot>> stream =
            SnapshotStream.error(new XdsResourceException(CLUSTER, clusterName,
                                                          "config source not found"));
    return stream.subscribe(watcher);
}

This prevents invalid EDS config sources from being silently used. The trade-off is that validation is now deferred from parse-time to usage-time, rather than failing fast during resource parsing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java` around
lines 28 - 31, The parse() method in ClusterResourceParser no longer performs
EDS-specific config_source validation and relies only on registry.assertValid();
restore the original EDS validation by calling
XdsConverterUtil.validateConfigSource(...) on the Cluster's eds.getEdsConfig()
inside ClusterResourceParser.parse before constructing the ClusterXdsResource
(so use XdsConverterUtil.validateConfigSource(eds.getEdsConfig()) and, if it
indicates invalid, throw or surface an XdsResourceException consistent with
previous behavior). This keeps fast-fail parsing while leaving ClusterStream's
runtime configSourceMapper.configSource() check as an additional safeguard.

Comment on lines +81 to +107
private SubscriptionHandler createClient(ConfigSource configSource) {
switch (configSource.getConfigSourceSpecifierCase()) {
case PATH_CONFIG_SOURCE:
case CUSTOM_CONFIG_SOURCE:
final SotwConfigSourceSubscriptionFactory streamFactory = resolveStreamFactory(configSource);
checkArgument(streamFactory != null,
"No SotwConfigSourceSubscriptionFactory found for: %s", configSource);
final StateCoordinator stateCoordinator =
new StateCoordinator(eventLoop, configSource, false);
final SubscriptionCallbacks callbacks =
new SubscriptionCallbacks(stateCoordinator, extensionRegistry);
final ConfigSourceSubscription stream =
streamFactory.create(configSource, callbacks, eventLoop);
return new ConfigSourceHandler(stateCoordinator, stream);
case ADS:
case API_CONFIG_SOURCE:
final GrpcConfigSourceStreamFactory grpcFactory =
extensionRegistry.queryByName(GrpcConfigSourceStreamFactory.NAME,
GrpcConfigSourceStreamFactory.class);
checkArgument(grpcFactory != null, "No GrpcConfigSourceStreamFactory registered");
return grpcFactory.create(
configSource, eventLoop, bootstrapNode, bootstrapClusters,
configSourceMapper, extensionRegistry);
default:
throw new IllegalArgumentException("Unsupported config source: " + configSource);
}
}
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.

⚠️ Potential issue | 🟡 Minor

Avoid embedding the full ConfigSource proto in error messages.

Lines 86-87 ("No SotwConfigSourceSubscriptionFactory found for: %s") and 105 ("Unsupported config source: " + configSource) both splat the entire ConfigSource message into the exception. ConfigSource can carry credentials/initial-metadata via its embedded ApiConfigSource -> GrpcService (see grpc_service.proto:initial_metadata, call credentials, etc.), and these will end up in logs or user-visible stack traces. Prefer reporting only the configSourceSpecifierCase (and, for CUSTOM_CONFIG_SOURCE, the typeUrl).

🛡️ Suggested change
-                checkArgument(streamFactory != null,
-                              "No SotwConfigSourceSubscriptionFactory found for: %s", configSource);
+                checkArgument(streamFactory != null,
+                              "No SotwConfigSourceSubscriptionFactory found for specifier %s (typeUrl=%s)",
+                              configSource.getConfigSourceSpecifierCase(),
+                              configSource.getCustomConfigSource().getTypeUrl());
@@
-            default:
-                throw new IllegalArgumentException("Unsupported config source: " + configSource);
+            default:
+                throw new IllegalArgumentException(
+                        "Unsupported config source specifier: " +
+                        configSource.getConfigSourceSpecifierCase());

As per coding guidelines: "Prefer meaningful exception messages with offending values and expected constraints."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java`
around lines 81 - 107, The exception messages in createClient currently
interpolate the full ConfigSource proto (in the checkArgument and
IllegalArgumentException); instead, change both messages to avoid printing the
entire proto by including only configSource.getConfigSourceSpecifierCase() and,
when the case is CUSTOM_CONFIG_SOURCE, also include
configSource.getCustomConfigSource().getTypeUrl() (or null-safe equivalent) to
identify the offending value; update the checkArgument call that references "No
SotwConfigSourceSubscriptionFactory found for: %s" and the
IllegalArgumentException that concatenates configSource so they use these safe,
minimal identifiers (reference: createClient, resolveStreamFactory,
ConfigSourceHandler, and the ConfigSourceSpecifierCase getters).

@jrhee17 jrhee17 force-pushed the feat/xds-support-field branch from 953fc02 to 8116fd8 Compare April 23, 2026 01:09
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (14)
xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java (1)

49-51: ⚠️ Potential issue | 🟠 Major

Resolve the PGV validator by message class and validate null explicitly.

Line 51 passes the message instance into validatorFor, but the PGV Java usage resolves by message.getClass() before validating the instance (PGV Java docs, lines 378-381). Also keep the public API contract deterministic with an explicit null check.

Proposed fix
+import static java.util.Objects.requireNonNull;
+
+import com.google.protobuf.Message;
+
@@
-    public void assertValid(Object message) {
+    public void assertValid(Message message) {
+        requireNonNull(message, "message");
         try {
-            delegate.validatorFor(message).assertValid(message);
+            delegate.validatorFor(message.getClass()).assertValid(message);

As per coding guidelines, “validate user-facing public method parameters”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java` around
lines 49 - 51, The public PgvValidator.assertValid method must explicitly
validate its parameter and resolve the PGV validator by the message class
instead of the instance; add a null check at the start of assertValid(Object
message) (throw IllegalArgumentException or NullPointerException per project
convention) and call delegate.validatorFor(message.getClass()) to obtain the
validator before invoking assertValid(message). Ensure you reference the
PgvValidator.assertValid method and the delegate.validatorFor(...) call when
making the change.
xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java (1)

45-48: ⚠️ Potential issue | 🟠 Major

stateCoordinator.close() still skipped if stream.close() throws.

Wrap in try/finally so the coordinator is always released even when ConfigSourceSubscription.close() fails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java` around
lines 45 - 48, The close() implementation in ConfigSourceHandler currently calls
stream.close() and then stateCoordinator.close(), which means
stateCoordinator.close() is skipped if stream.close() throws; change close() to
call stream.close() inside a try block and ensure stateCoordinator.close() is
invoked in a finally block (so ConfigSourceSubscription.close()/stream.close()
failures cannot prevent stateCoordinator.close()), referencing the close()
method of ConfigSourceHandler, the stream.close() call
(ConfigSourceSubscription.close()), and stateCoordinator.close().
xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java (1)

49-52: ⚠️ Potential issue | 🟡 Minor

Reject null message at the API boundary.

Same concern as before: assertValid(null) falls through to PgvValidator and throws a less informative NPE. Prefer an explicit requireNonNull(message, "message") on this public method. As per coding guidelines, "validate user-facing public method parameters".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java`
around lines 49 - 52, Add an explicit null check at the API boundary in
DefaultXdsValidatorIndex.assertValid to reject a null message before delegating
to internal validators: call Objects.requireNonNull(message, "message") (or
equivalent) at the start of assertValid, then proceed to invoke
pgvValidator.assertValid(message) and supportedFieldValidator.validate(message)
so any null input yields a clear NPE message referencing the parameter instead
of a downstream, less-informative failure.
xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java (1)

43-54: ⚠️ Potential issue | 🟡 Minor

Reject null message at the API boundary.

assertValid(null) currently surfaces a confusing downstream NPE from PgvValidator/SupportedFieldValidator. Add requireNonNull(message, "message") at the start so the public contract fails with a stable error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java`
around lines 43 - 54, In assertValid(Message message) add an explicit null check
at the API boundary: call Objects.requireNonNull(message, "message") as the
first statement in the method (in StrictXdsValidatorIndex.assertValid) so
passing null fails with a clear NPE message instead of letting
PgvValidator/SupportedFieldValidator produce a confusing downstream NPE; import
java.util.Objects if needed.
xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java (2)

154-173: ⚠️ Potential issue | 🟠 Major

Terminal callbacks still run after local close.

closeStream() flips completed, but onError/onCompleted don't re-check the flag before invoking retryOrClose(...), so a graceful shutdown can be mistaken for a reconnect-worthy error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java` around
lines 154 - 173, onError and onCompleted invoke lifecycleObserver and
owner.retryOrClose even after closeStream sets completed; update both methods
(onError and onCompleted) to re-check the completed flag after dispatching to
the eventLoop and before calling lifecycleObserver.streamError/streamCompleted
and owner.retryOrClose, returning early if completed is true so local close
won’t trigger a retry/ reconnect; ensure the same completed check exists in both
the in-event-loop path and the remote-dispatch path to avoid races with
closeStream.

145-149: ⚠️ Potential issue | 🟠 Major

Unknown typeUrl silently dropped (no NACK).

Same concern as the delta stream: logging and returning leaves the response neither ACKed nor NACKed, so the management server is left waiting on the nonce.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java` around
lines 145 - 149, The code in SotwActualStream currently drops unknown typeUrls
by logging and returning when
XdsResourceParserUtil.fromTypeUrl(value.getTypeUrl()) returns null; change this
to NACK the discovery response instead of silently returning: in the branch
where parser == null, invoke the existing NACK mechanism used elsewhere in
SotwActualStream (include the incoming response nonce via value.getNonce() and a
clear error message mentioning the unknown typeUrl from value.getTypeUrl()), so
the management server receives a NACK with error details rather than leaving the
nonce unacknowledged.
xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java (2)

232-251: ⚠️ Potential issue | 🟠 Major

Terminal callbacks still run after local close.

closeStream() sets completed = true, but onError / onCompleted don't re-check it before invoking the lifecycle observer and owner.retryOrClose(...), so an intentional local close can be treated as a remote termination and trigger an unwanted retry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java` around
lines 232 - 251, The onError and onCompleted handlers must ignore callbacks if
the stream was already closed locally: add a guard that returns immediately when
completed is true at the start of both onError(Throwable) and onCompleted()
(after the eventLoop check/scheduling so the check runs on the event loop
thread); only call lifecycleObserver.streamError/streamCompleted and
owner.retryOrClose(...) when completed was false and then set completed = true
as currently done. This uses the existing symbols onError, onCompleted,
completed, closeStream(), lifecycleObserver.streamError/streamCompleted, and
owner.retryOrClose to locate and patch the logic.

223-227: ⚠️ Potential issue | 🟠 Major

Unknown typeUrl still silently dropped (no NACK).

When XdsResourceParserUtil.fromTypeUrl(...) returns null, the stream logs and returns without ACKing or NACKing, so the management server never learns about the unexpected type and waits indefinitely on that nonce. Consider NACKing with the received nonce and a descriptive error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java` around
lines 223 - 227, DeltaActualStream currently just logs and returns when
XdsResourceParserUtil.fromTypeUrl(value.getTypeUrl()) is null, leaving the
server waiting; change this branch to send a NACK back to the control plane
using the stream's existing NACK/response mechanism (include value.getNonce()
and a descriptive error mentioning the unexpected typeUrl from
value.getTypeUrl()) instead of silently returning so the management server
receives the rejection and can proceed; update the branch inside
DeltaActualStream where parser == null to construct and send that NACK and then
return.
xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java (1)

49-56: ⚠️ Potential issue | 🟡 Minor

Defensively copy downstreamFilters before deriving router.

The resource stores the caller-owned list and computes router once from it. A later mutation can make router() inconsistent with downstreamFilters() and is also reused by withRevision(...).

Proposed fix
         super(version, revision);
         this.listener = listener;
         this.connectionManager = connectionManager;
-        this.downstreamFilters = downstreamFilters;
-        this.router = findRouter(downstreamFilters);
+        this.downstreamFilters = ImmutableList.copyOf(downstreamFilters);
+        this.router = findRouter(this.downstreamFilters);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java` around
lines 49 - 56, The constructor of ListenerXdsResource stores the caller-owned
list downstreamFilters and then derives router from it, which can lead to
inconsistencies if the caller mutates the list later; defensively copy
downstreamFilters (e.g., new ArrayList<>(downstreamFilters)) at the start of the
ListenerXdsResource(...) constructor before calling findRouter(...) and
assigning this.downstreamFilters, and ensure any factory/clone methods such as
withRevision(...) also use copies so router() remains consistent with
downstreamFilters().
xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java (1)

76-85: ⚠️ Potential issue | 🟡 Minor

Add explicit null checks before unpacking.

unpack(...) is public API and still accepts message / clazz without validation, so null inputs fail with less useful errors inside protobuf code.

Proposed fix
+import static java.util.Objects.requireNonNull;
+
 ...
     public <T extends Message> T unpack(Any message, Class<T> clazz) {
+        requireNonNull(message, "message");
+        requireNonNull(clazz, "clazz");
         final T unpacked;

As per coding guidelines: “validate user-facing public method parameters”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java` around
lines 76 - 85, The public method unpack(Any message, Class<T> clazz) must
validate its parameters up front: add explicit null checks for message and clazz
at the top of unpack and throw descriptive NullPointerException(s) (e.g., via
Objects.requireNonNull with messages) before attempting message.unpack(clazz);
keep the existing try/catch for InvalidProtocolBufferException and the
subsequent call to assertValid(unpacked) unchanged so behavior is preserved for
non-null inputs.
xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java (1)

34-41: ⚠️ Potential issue | 🟡 Minor

Close the shared DirectoryWatchService after the test class.

WATCH_SERVICE is static and currently never closed, so the watcher resources can outlive the test run.

🧹 Proposed fix
+import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Test;
@@
     private static XdsExtensionRegistry createRegistry() {
         return XdsExtensionRegistry.of(VALIDATOR, WATCH_SERVICE,
                                        Flags.meterRegistry(),
                                        new MeterIdPrefix("armeria.xds"));
     }
+
+    `@AfterAll`
+    static void closeWatchService() {
+        WATCH_SERVICE.close();
+    }

Optional verification:

#!/bin/bash
rg -n -C3 'WATCH_SERVICE|@AfterAll|DirectoryWatchService' --type java
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java`
around lines 34 - 41, The static DirectoryWatchService WATCH_SERVICE is never
closed, leaking watcher resources; add a static teardown to
XdsExtensionRegistryTest that closes it after all tests run. Implement a method
annotated with `@AfterAll` (static) in XdsExtensionRegistryTest which calls
WATCH_SERVICE.close() (or WATCH_SERVICE.shutdown/stop if appropriate) to release
resources; ensure imports for `@AfterAll` are present and that createRegistry()
continues to use the same WATCH_SERVICE.
xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java (1)

94-103: ⚠️ Potential issue | 🟠 Major

Validate every configured GrpcService and keep errors sanitized.

Line 101 only checks the first service, but Line 106 passes the full list to GrpcServicesPreprocessor, so unsupported later entries still slip through. Lines 94, 99, and 102 also stringify the full ConfigSource; report the specifier and failing index instead.

🛠️ Proposed fix
             } else {
-                throw new IllegalArgumentException("Unsupported config source: " + configSource);
+                throw new IllegalArgumentException(
+                        "Unsupported config source specifier: " +
+                        configSource.getConfigSourceSpecifierCase());
             }
 
             final List<GrpcService> grpcServices = apiConfigSource.getGrpcServicesList();
             checkArgument(!grpcServices.isEmpty(),
-                          "At least one GrpcService should be specified for '%s'", configSource);
+                          "At least one GrpcService should be specified for config source specifier %s",
+                          configSource.getConfigSourceSpecifierCase());
+            for (int i = 0; i < grpcServices.size(); i++) {
+                final GrpcService grpcService = grpcServices.get(i);
+                checkArgument(grpcService.hasEnvoyGrpc(),
+                              "Only envoyGrpc is supported for GrpcService at index %s " +
+                              "(config source specifier: %s)",
+                              i, configSource.getConfigSourceSpecifierCase());
+            }
             final GrpcService firstGrpcService = grpcServices.get(0);
-            checkArgument(firstGrpcService.hasEnvoyGrpc(),
-                          "Only envoyGrpc is supported for '%s'", configSource);
             final EnvoyGrpc envoyGrpc = firstGrpcService.getEnvoyGrpc();

You can verify there are no remaining first-only validations with:

#!/bin/bash
rg -n -C3 'getGrpcServicesList|hasEnvoyGrpc|Unsupported config source|At least one GrpcService' --type java

As per coding guidelines: “meaningful exception messages (include offending value + expected values)”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java`
around lines 94 - 103, The code in GrpcConfigSourceStreamFactory validates only
the first GrpcService (apiConfigSource.getGrpcServicesList() /
firstGrpcService.hasEnvoyGrpc()) but later passes the full list to
GrpcServicesPreprocessor, and exception messages stringify the entire
ConfigSource; update validation to iterate over every GrpcService in
apiConfigSource.getGrpcServicesList() and for each index ensure hasEnvoyGrpc()
is true and any other supported constraints, throwing IllegalArgumentException
that includes the configSource specifier plus the failing index (e.g.
"GrpcService[2] must use envoyGrpc; found X") instead of stringifying the whole
ConfigSource, and then pass the validated list to GrpcServicesPreprocessor as
before.
xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java (1)

110-112: ⚠️ Potential issue | 🟡 Minor

Decode the watched JSON as UTF-8 explicitly.

Line 111 uses the platform default charset, which can make path config parsing environment-dependent.

🔧 Proposed fix
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@
                 final DiscoveryResponse.Builder builder = DiscoveryResponse.newBuilder();
-                JsonFormat.parser().ignoringUnknownFields().merge(new String(bytes), builder);
+                JsonFormat.parser().ignoringUnknownFields()
+                          .merge(new String(bytes, StandardCharsets.UTF_8), builder);
                 response = builder.build();

Optional verification:

#!/bin/bash
rg -n -C2 'new String\(bytes\)' --type java
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java`
around lines 110 - 112, The code constructs a String from raw bytes using the
platform default charset in PathSotwConfigSourceSubscriptionFactory (where
DiscoveryResponse.Builder is populated via JsonFormat.parser().merge(new
String(bytes), builder)); change this to decode the bytes explicitly as UTF-8
(use StandardCharsets.UTF_8) and add the necessary import
(java.nio.charset.StandardCharsets) so the JSON is consistently parsed across
environments.
xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java (1)

86-87: ⚠️ Potential issue | 🟡 Minor

Avoid including the full ConfigSource in exceptions.

Line 87 and Line 105 still stringify the entire proto. Prefer the specifier and, for custom sources, the custom type URL to avoid leaking embedded gRPC metadata or credentials.

🛡️ Proposed fix
                 final SotwConfigSourceSubscriptionFactory streamFactory = resolveStreamFactory(configSource);
                 checkArgument(streamFactory != null,
-                              "No SotwConfigSourceSubscriptionFactory found for: %s", configSource);
+                              "No SotwConfigSourceSubscriptionFactory found for specifier %s " +
+                              "(typeUrl: %s)",
+                              configSource.getConfigSourceSpecifierCase(),
+                              configSource.hasCustomConfigSource() ?
+                                      configSource.getCustomConfigSource().getTypeUrl() : null);
@@
             default:
-                throw new IllegalArgumentException("Unsupported config source: " + configSource);
+                throw new IllegalArgumentException(
+                        "Unsupported config source specifier: " +
+                        configSource.getConfigSourceSpecifierCase());
         }

As per coding guidelines: “meaningful exception messages (include offending value + expected values)”.

Also applies to: 104-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java`
around lines 86 - 87, The exception message currently stringifies the entire
ConfigSource when calling checkArgument in ControlPlaneClientManager (the
streamFactory != null check with configSource), which can leak sensitive proto
contents; change the message to include only the ConfigSource specifier and, for
custom sources, the custom type URL instead of the full proto (e.g. use
configSource.getSpecifierCase() and when specifier is CUSTOM include
configSource.getCustom().getTypeUrl()) so the checkArgument call (and the other
similar check using configSource) logs a minimal, non-sensitive identifier
rather than the whole ConfigSource.
🧹 Nitpick comments (2)
xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java (1)

51-51: Redundant cast.

message is already typed as com.google.protobuf.Message, so (Message) message is a no-op.

♻️ Proposed cleanup
-        supportedFieldValidator.validate((Message) message);
+        supportedFieldValidator.validate(message);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java`
at line 51, In DefaultXdsValidatorIndex, remove the redundant cast when calling
supportedFieldValidator.validate: replace the call using (Message) message with
a direct call to validate(message) since message is already a
com.google.protobuf.Message; update the invocation in the method containing
supportedFieldValidator.validate so it passes message directly (no other changes
needed to imports or types).
xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java (1)

45-49: Per-call SupportedFieldValidator allocation defeats the descriptor cache.

SupportedFieldValidator.of(handler) constructs a fresh validator for every non-ignore handler, so its internal supportedFieldsCache (ConcurrentMap<Descriptor, Set<FieldDescriptor>>) is rebuilt on each assertValid(...) call. Since this validator is wired into the hot xDS parsing path via SPI, rebuilding the reflection-derived supported-field set per message is noticeably wasteful.

Consider either (a) hoisting the cache to a static/shared structure, (b) exposing a validate(Message, UnsupportedFieldHandler) overload on SupportedFieldValidator that reuses a cached instance, or (c) using a reusable mutable collector (e.g., a thread-local/AtomicReference<List<String>>) so a single validator instance can be retained as a field here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java`
around lines 45 - 49, The code creates a new SupportedFieldValidator per message
via SupportedFieldValidator.of(...) which rebuilds its internal
supportedFieldsCache each time and defeats the descriptor cache; to fix, stop
allocating per-call validators in StrictXdsValidatorIndex: either obtain and
reuse a shared/reusable SupportedFieldValidator instance (hoist to a
static/shared field) or update SupportedFieldValidator to add a
validate(Message, UnsupportedFieldHandler) overload so you can call a cached
validator instance, or use a thread-local/AtomicReference-backed mutable
collector for the violations list so a single validator instance can be stored
as a field on StrictXdsValidatorIndex and reused across validate(message) calls
instead of reconstructing it each time. Ensure the change references
SupportedFieldValidator.of, SupportedFieldValidator.assertValid/validate and the
StrictXdsValidatorIndex field where the validator will be stored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@xds-validator/build.gradle`:
- Line 2: The build currently declares the protobuf dependency with
implementation (implementation libs.protobuf.java) but
com.google.protobuf.Message is exposed in the public API via
XdsValidatorIndex.assertValid(Message message); change the dependency
declaration from implementation to api so protobuf is available on consumers'
compile classpath and transitively exposed for implementers/callers of
XdsValidatorIndex.assertValid(Message).

---

Duplicate comments:
In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java`:
- Around line 49-52: Add an explicit null check at the API boundary in
DefaultXdsValidatorIndex.assertValid to reject a null message before delegating
to internal validators: call Objects.requireNonNull(message, "message") (or
equivalent) at the start of assertValid, then proceed to invoke
pgvValidator.assertValid(message) and supportedFieldValidator.validate(message)
so any null input yields a clear NPE message referencing the parameter instead
of a downstream, less-informative failure.

In `@xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java`:
- Around line 49-51: The public PgvValidator.assertValid method must explicitly
validate its parameter and resolve the PGV validator by the message class
instead of the instance; add a null check at the start of assertValid(Object
message) (throw IllegalArgumentException or NullPointerException per project
convention) and call delegate.validatorFor(message.getClass()) to obtain the
validator before invoking assertValid(message). Ensure you reference the
PgvValidator.assertValid method and the delegate.validatorFor(...) call when
making the change.

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java`:
- Around line 43-54: In assertValid(Message message) add an explicit null check
at the API boundary: call Objects.requireNonNull(message, "message") as the
first statement in the method (in StrictXdsValidatorIndex.assertValid) so
passing null fails with a clear NPE message instead of letting
PgvValidator/SupportedFieldValidator produce a confusing downstream NPE; import
java.util.Objects if needed.

In `@xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java`:
- Around line 45-48: The close() implementation in ConfigSourceHandler currently
calls stream.close() and then stateCoordinator.close(), which means
stateCoordinator.close() is skipped if stream.close() throws; change close() to
call stream.close() inside a try block and ensure stateCoordinator.close() is
invoked in a finally block (so ConfigSourceSubscription.close()/stream.close()
failures cannot prevent stateCoordinator.close()), referencing the close()
method of ConfigSourceHandler, the stream.close() call
(ConfigSourceSubscription.close()), and stateCoordinator.close().

In `@xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java`:
- Around line 86-87: The exception message currently stringifies the entire
ConfigSource when calling checkArgument in ControlPlaneClientManager (the
streamFactory != null check with configSource), which can leak sensitive proto
contents; change the message to include only the ConfigSource specifier and, for
custom sources, the custom type URL instead of the full proto (e.g. use
configSource.getSpecifierCase() and when specifier is CUSTOM include
configSource.getCustom().getTypeUrl()) so the checkArgument call (and the other
similar check using configSource) logs a minimal, non-sensitive identifier
rather than the whole ConfigSource.

In `@xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java`:
- Around line 232-251: The onError and onCompleted handlers must ignore
callbacks if the stream was already closed locally: add a guard that returns
immediately when completed is true at the start of both onError(Throwable) and
onCompleted() (after the eventLoop check/scheduling so the check runs on the
event loop thread); only call lifecycleObserver.streamError/streamCompleted and
owner.retryOrClose(...) when completed was false and then set completed = true
as currently done. This uses the existing symbols onError, onCompleted,
completed, closeStream(), lifecycleObserver.streamError/streamCompleted, and
owner.retryOrClose to locate and patch the logic.
- Around line 223-227: DeltaActualStream currently just logs and returns when
XdsResourceParserUtil.fromTypeUrl(value.getTypeUrl()) is null, leaving the
server waiting; change this branch to send a NACK back to the control plane
using the stream's existing NACK/response mechanism (include value.getNonce()
and a descriptive error mentioning the unexpected typeUrl from
value.getTypeUrl()) instead of silently returning so the management server
receives the rejection and can proceed; update the branch inside
DeltaActualStream where parser == null to construct and send that NACK and then
return.

In
`@xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java`:
- Around line 94-103: The code in GrpcConfigSourceStreamFactory validates only
the first GrpcService (apiConfigSource.getGrpcServicesList() /
firstGrpcService.hasEnvoyGrpc()) but later passes the full list to
GrpcServicesPreprocessor, and exception messages stringify the entire
ConfigSource; update validation to iterate over every GrpcService in
apiConfigSource.getGrpcServicesList() and for each index ensure hasEnvoyGrpc()
is true and any other supported constraints, throwing IllegalArgumentException
that includes the configSource specifier plus the failing index (e.g.
"GrpcService[2] must use envoyGrpc; found X") instead of stringifying the whole
ConfigSource, and then pass the validated list to GrpcServicesPreprocessor as
before.

In `@xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java`:
- Around line 49-56: The constructor of ListenerXdsResource stores the
caller-owned list downstreamFilters and then derives router from it, which can
lead to inconsistencies if the caller mutates the list later; defensively copy
downstreamFilters (e.g., new ArrayList<>(downstreamFilters)) at the start of the
ListenerXdsResource(...) constructor before calling findRouter(...) and
assigning this.downstreamFilters, and ensure any factory/clone methods such as
withRevision(...) also use copies so router() remains consistent with
downstreamFilters().

In
`@xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java`:
- Around line 110-112: The code constructs a String from raw bytes using the
platform default charset in PathSotwConfigSourceSubscriptionFactory (where
DiscoveryResponse.Builder is populated via JsonFormat.parser().merge(new
String(bytes), builder)); change this to decode the bytes explicitly as UTF-8
(use StandardCharsets.UTF_8) and add the necessary import
(java.nio.charset.StandardCharsets) so the JSON is consistently parsed across
environments.

In `@xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java`:
- Around line 154-173: onError and onCompleted invoke lifecycleObserver and
owner.retryOrClose even after closeStream sets completed; update both methods
(onError and onCompleted) to re-check the completed flag after dispatching to
the eventLoop and before calling lifecycleObserver.streamError/streamCompleted
and owner.retryOrClose, returning early if completed is true so local close
won’t trigger a retry/ reconnect; ensure the same completed check exists in both
the in-event-loop path and the remote-dispatch path to avoid races with
closeStream.
- Around line 145-149: The code in SotwActualStream currently drops unknown
typeUrls by logging and returning when
XdsResourceParserUtil.fromTypeUrl(value.getTypeUrl()) returns null; change this
to NACK the discovery response instead of silently returning: in the branch
where parser == null, invoke the existing NACK mechanism used elsewhere in
SotwActualStream (include the incoming response nonce via value.getNonce() and a
clear error message mentioning the unknown typeUrl from value.getTypeUrl()), so
the management server receives a NACK with error details rather than leaving the
nonce unacknowledged.

In `@xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java`:
- Around line 76-85: The public method unpack(Any message, Class<T> clazz) must
validate its parameters up front: add explicit null checks for message and clazz
at the top of unpack and throw descriptive NullPointerException(s) (e.g., via
Objects.requireNonNull with messages) before attempting message.unpack(clazz);
keep the existing try/catch for InvalidProtocolBufferException and the
subsequent call to assertValid(unpacked) unchanged so behavior is preserved for
non-null inputs.

In `@xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java`:
- Around line 34-41: The static DirectoryWatchService WATCH_SERVICE is never
closed, leaking watcher resources; add a static teardown to
XdsExtensionRegistryTest that closes it after all tests run. Implement a method
annotated with `@AfterAll` (static) in XdsExtensionRegistryTest which calls
WATCH_SERVICE.close() (or WATCH_SERVICE.shutdown/stop if appropriate) to release
resources; ensure imports for `@AfterAll` are present and that createRegistry()
continues to use the same WATCH_SERVICE.

---

Nitpick comments:
In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java`:
- Line 51: In DefaultXdsValidatorIndex, remove the redundant cast when calling
supportedFieldValidator.validate: replace the call using (Message) message with
a direct call to validate(message) since message is already a
com.google.protobuf.Message; update the invocation in the method containing
supportedFieldValidator.validate so it passes message directly (no other changes
needed to imports or types).

In
`@xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java`:
- Around line 45-49: The code creates a new SupportedFieldValidator per message
via SupportedFieldValidator.of(...) which rebuilds its internal
supportedFieldsCache each time and defeats the descriptor cache; to fix, stop
allocating per-call validators in StrictXdsValidatorIndex: either obtain and
reuse a shared/reusable SupportedFieldValidator instance (hoist to a
static/shared field) or update SupportedFieldValidator to add a
validate(Message, UnsupportedFieldHandler) overload so you can call a cached
validator instance, or use a thread-local/AtomicReference-backed mutable
collector for the violations list so a single validator instance can be stored
as a field on StrictXdsValidatorIndex and reused across validate(message) calls
instead of reconstructing it each time. Ensure the change references
SupportedFieldValidator.of, SupportedFieldValidator.assertValid/validate and the
StrictXdsValidatorIndex field where the validator will be stored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f82f2ea5-b569-4df7-9b2f-eca2b36e5609

📥 Commits

Reviewing files that changed from the base of the PR and between 953fc02 and 8116fd8.

📒 Files selected for processing (83)
  • it/xds-client/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • it/xds-no-validation/src/test/java/com/linecorp/armeria/xds/it/NoopXdsValidatorIndex.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/DefaultXdsValidatorIndex.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/PgvValidator.java
  • xds-api/src/main/java/com/linecorp/armeria/xds/api/StrictXdsValidatorIndex.java
  • xds-api/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto
  • xds-api/src/main/proto/envoy/config/cluster/v3/cluster.proto
  • xds-api/src/main/proto/envoy/config/core/v3/address.proto
  • xds-api/src/main/proto/envoy/config/core/v3/base.proto
  • xds-api/src/main/proto/envoy/config/core/v3/config_source.proto
  • xds-api/src/main/proto/envoy/config/core/v3/grpc_service.proto
  • xds-api/src/main/proto/envoy/config/core/v3/health_check.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint_components.proto
  • xds-api/src/main/proto/envoy/config/listener/v3/api_listener.proto
  • xds-api/src/main/proto/envoy/config/listener/v3/listener.proto
  • xds-api/src/main/proto/envoy/config/route/v3/route.proto
  • xds-api/src/main/proto/envoy/config/route/v3/route_components.proto
  • xds-api/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto
  • xds-api/src/main/proto/envoy/type/matcher/v3/regex.proto
  • xds-api/src/main/proto/envoy/type/matcher/v3/string.proto
  • xds-api/src/main/proto/envoy/type/v3/percent.proto
  • xds-api/src/main/proto/envoy/type/v3/range.proto
  • xds-validator/build.gradle
  • xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceClient.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceSubscription.java
  • xds/src/main/java/com/linecorp/armeria/xds/ControlPlaneClientManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/DefaultSubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/DeltaActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/GrpcConfigSourceStreamFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/HttpConnectionManagerFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/PathSotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/RawBufferTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceNodeAdapter.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwActualStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SotwSubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/StateCoordinator.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/UpstreamTlsTransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/VirtualHostXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapImpl.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsClusterManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsConverterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceValidator.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsClientCleanupTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsExtensionRegistryTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsResourceValidatorTest.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistryTest.java
  • xds/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
💤 Files with no reviewable changes (10)
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/VirtualHostXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsConverterUtil.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistryTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterXdsResource.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsValidatorIndexRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceClient.java
✅ Files skipped from review due to trivial changes (20)
  • xds/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • xds/src/main/java/com/linecorp/armeria/xds/XdsClusterManager.java
  • it/xds-client/src/test/resources/META-INF/services/com.linecorp.armeria.xds.validator.XdsValidatorIndex
  • xds/src/main/java/com/linecorp/armeria/xds/SotwConfigSourceSubscriptionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionHandler.java
  • xds/src/main/java/com/linecorp/armeria/xds/RawBufferTransportSocketFactory.java
  • xds/src/test/java/com/linecorp/armeria/xds/XdsClientCleanupTest.java
  • xds-api/src/main/proto/envoy/config/core/v3/address.proto
  • xds-api/src/main/proto/envoy/type/v3/range.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/secret.proto
  • xds-api/src/main/proto/envoy/config/core/v3/base.proto
  • xds-api/src/main/proto/envoy/config/endpoint/v3/endpoint_components.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/tls.proto
  • xds-api/src/main/proto/envoy/config/bootstrap/v3/bootstrap.proto
  • xds-api/src/main/proto/envoy/config/route/v3/route_components.proto
  • xds-api/src/main/proto/envoy/config/core/v3/health_check.proto
  • xds-api/src/main/proto/envoy/config/core/v3/config_source.proto
  • xds-api/src/main/proto/envoy/extensions/transport_sockets/tls/v3/common.proto
  • xds-api/src/main/proto/envoy/config/cluster/v3/cluster.proto
🚧 Files skipped from review as they are similar to previous changes (24)
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionContext.java
  • xds/src/main/java/com/linecorp/armeria/xds/ResourceNodeAdapter.java
  • xds-validator/src/main/java/com/linecorp/armeria/xds/validator/XdsValidatorIndex.java
  • xds/src/main/java/com/linecorp/armeria/xds/ConfigSourceSubscription.java
  • xds/src/main/java/com/linecorp/armeria/xds/TransportSocketFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/EndpointResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/HttpConnectionManagerFactory.java
  • xds/src/main/java/com/linecorp/armeria/xds/SecretResourceParser.java
  • xds-api/src/main/proto/envoy/config/listener/v3/api_listener.proto
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerManager.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteResourceParser.java
  • xds-api/src/main/proto/envoy/config/listener/v3/listener.proto
  • xds/src/test/java/com/linecorp/armeria/xds/XdsResourceValidatorTest.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsResourceParserUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsUnpackUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/SubscriptionCallbacks.java
  • xds/src/main/java/com/linecorp/armeria/xds/ClusterResourceParser.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsExtensionRegistry.java
  • xds/src/main/java/com/linecorp/armeria/xds/XdsBootstrapImpl.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java
  • xds-api/src/main/proto/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto

Comment thread xds-validator/build.gradle Outdated
@jrhee17 jrhee17 force-pushed the feat/xds-support-field branch from 7a5fe0d to f81ce25 Compare April 23, 2026 01:41
jrhee17 added a commit that referenced this pull request Apr 30, 2026
This PR is a subset of #6721

## Motivation

The xDS module previously scattered extension handling (HTTP filters,
transport sockets, network filters) across multiple static registries
and utility classes (`HttpFilterFactoryRegistry`,
`XdsValidatorIndexRegistry`, `XdsConverterUtil`), each with inconsistent
lookup and validation behavior. Validation was also performed
redundantly — both during resource parsing and in individual
`XdsResource` constructors — which could be expensive due to full
message tree traversal.

This PR introduces `XdsExtensionRegistry` as the single,
bootstrap-scoped registry that unifies extension factory lookup, proto
`Any` unpacking, and validation.

## Modifications

### Introduce `XdsExtensionRegistry` to unify unpack/validation behavior
- Added `XdsExtensionFactory` as the base interface for all extension
factories, providing `name()` and `typeUrls()` for dual-key resolution
(type URL primary, extension name fallback).
- Added `XdsExtensionRegistry` as the central registry that discovers
factories via SPI and built-in registration. Provides `query()` for
factory lookup, `unpack()` for `Any` fields, and `assertValid()` for
proto validation.
- The registry is created once per bootstrap in `XdsBootstrapImpl` and
threaded through the pipeline via `SubscriptionContext`, rather than
being a static singleton. This allows users to inject custom factories
via the bootstrap builder in the future.
- Removed `HttpFilterFactoryRegistry` and `XdsValidatorIndexRegistry`
which are now subsumed by `XdsExtensionRegistry`.

### Consolidate validation into three well-defined points
- Added `XdsResourceValidator` which delegates to the highest-priority
`XdsValidatorIndex` loaded via SPI. Validation now happens at exactly
three points:
1. **Bootstrap**: The entire `Bootstrap` proto is validated once at
construction time.
2. **Dynamic resources**: Each resource from a config source is
validated on arrival in `ResourceParser.parseResources()` /
`parseDeltaResources()`.
3. **Any unpacking**: Opaque `Any` fields are validated when unpacked
via `registry.unpack()`.
- Removed per-resource validation calls from `ClusterXdsResource`,
`ListenerXdsResource`, `EndpointXdsResource`, `RouteXdsResource`,
`SecretXdsResource`, and `VirtualHostXdsResource` constructors. This
avoids redundant full-message-tree traversals.
- Removed `XdsConverterUtil` whose config source validation logic is
subsumed by the centralized validation.

### HTTP filters now use the extension registry
- `HttpFilterFactory` now extends `XdsExtensionFactory`; `create()`
receives `XdsResourceValidator` for config unpacking.
- `RouterFilterFactory` implements `name()` / `typeUrls()` and now
properly unpacks the `Router` proto. Extracted `RouterXdsHttpFilter` as
a public inner class with a `router()` getter.
- `FilterUtil` lookups changed from `HttpFilterFactoryRegistry` to
`XdsExtensionRegistry.query()`.
- Misc) Moved downstream HTTP filter chain from `ListenerSnapshot` (one
per listener) to `RouteEntry` (one per route), so downstream filters
receive per-route `typed_per_filter_config` overrides — matching Envoy's
behavior of resolving the route before running the HCM filter chain.

### Transport sockets now use the extension registry
- Added `TransportSocketFactory` as the extension factory interface for
transport sockets.
- Added `UpstreamTlsTransportSocketFactory` which encapsulates the TLS
logic previously hardcoded in `TransportSocketStream`.
- Added `RawBufferTransportSocketFactory` as a simple pass-through for
plaintext transport sockets.
- Refactored `TransportSocketStream` from monolithic TLS handling to a
strategy pattern — queries the registry for the appropriate
`TransportSocketFactory` and delegates creation.

### Listener unpacking centralized via `XdsUnpackUtil`
- Added `XdsUnpackUtil` to centralize unpacking of
`HttpConnectionManager` from listener configs and downstream filter
resolution.
- Added `HttpConnectionManagerFactory` for parsing
`HttpConnectionManager` network filter configs.
- `ListenerResourceParser` / `ListenerXdsResource`: Downstream filters
are resolved at parse time via `XdsUnpackUtil` and stored as a field,
rather than computed on demand.

## Result

- Extension resolution (HTTP filters, transport sockets, network
filters) is unified under `XdsExtensionRegistry` with dual-key lookup
(type URL + name).
- Proto validation is consolidated into three well-defined points,
avoiding redundant and expensive full-message-tree traversals.
- The registry is bootstrap-scoped (instance, not static), paving the
way for user-injected custom factories, supported-field validation, and
custom config sources.
@jrhee17 jrhee17 force-pushed the feat/xds-support-field branch from f81ce25 to 1ed8732 Compare May 4, 2026 06:48
@jrhee17 jrhee17 closed this May 4, 2026
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