Skip to content

Allow XdsFilter to handle unknown Message config#6710

Merged
jrhee17 merged 4 commits intoline:mainfrom
jrhee17:poc/istio-step5
Apr 9, 2026
Merged

Allow XdsFilter to handle unknown Message config#6710
jrhee17 merged 4 commits intoline:mainfrom
jrhee17:poc/istio-step5

Conversation

@jrhee17
Copy link
Copy Markdown
Contributor

@jrhee17 jrhee17 commented Apr 7, 2026

Subset of #6700

Motivation

HttpFilterFactory<T extends Message> forced each factory implementation to declare a specific protobuf Message type for its config, and required the framework to own config parsing and FilterConfig envelope unwrapping. This made it difficult to handle unknown or dynamically-typed xDS filter configs (e.g. Istio-specific filters), and leaked framework concerns into the factory API.

Additionally, ParsedFilterConfig wrapped per-route filter configs and required the framework to merge and propagate them through the snapshot hierarchy (RouteSnapshotVirtualHostSnapshotRouteEntry), coupling snapshot construction to filter logic.

Modifications

  • Replace HttpFilterFactory<T extends Message> with an untyped HttpFilterFactory interface; create(HttpFilter, Any) now receives the raw Any config directly, and factories are responsible for all parsing including FilterConfig envelope unwrapping. Returning null skips the filter.
  • Introduce XdsHttpFilter as the return type of create(), with default no-op httpPreprocessor(), rpcPreprocessor(), httpDecorator(), and rpcDecorator() methods.
  • Delete ParsedFilterConfig; per-route filter configs are now stored and merged as Map<String, Any> directly.
  • Move 3-level typed_per_filter_config merging (route-config → vhost → route) into RouteEntry's constructor, eliminating withFilterConfigs() from VirtualHostSnapshot and withRouter() from RouteSnapshot.
  • Thread @Nullable ListenerXdsResource through RouteStream inner classes so RouteEntry can access upstream HTTP filters from the Router directly.
  • Update RouterFilterFactory to implement the new untyped interface.
  • Simplify FilterUtil: remove toParsedFilterConfigs(), update mergeFilterConfigs to operate on Map<String, Any>, buildUpstreamFilter takes @Nullable RetryPolicy directly.

Result

  • HttpFilterFactory implementations no longer need to declare a protobuf type parameter, and can handle any xDS filter config including unknown or Istio-specific ones.
  • Per-route filter config merging is self-contained in RouteEntry rather than spread across the snapshot hierarchy.
  • RouteEntry.filterConfig(String) now returns @Nullable Any instead of @Nullable ParsedFilterConfig.

Breaking

  • HttpFilterFactory implementations must now implement XdsHttpFilter create(HttpFilter httpFilter, Any config) for custom filter implementations

@jrhee17 jrhee17 added this to the 1.39.0 milestone Apr 7, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1eab75f2-5540-44d7-a785-3a77b23b8536

📥 Commits

Reviewing files that changed from the base of the PR and between afb98b3 and 558fce6.

📒 Files selected for processing (1)
  • xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java

📝 Walkthrough

Walkthrough

Replaced parsed per-filter configs with raw protobuf Any, simplified the HttpFilterFactory API to accept raw Any and return XdsHttpFilter (nullable), moved filter-config merging into route/entry construction, and threaded listener context through route/virtual-host stream construction.

Changes

Cohort / File(s) Summary
Filter Factory API Redesign
xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactory.java, xds/src/main/java/com/linecorp/armeria/xds/filter/XdsHttpFilter.java, xds/src/main/java/com/linecorp/armeria/xds/filter/HttpFilterFactoryRegistry.java
Replaced generic HttpFilterFactory<T> with non-generic HttpFilterFactory that accepts raw Any and returns @Nullable XdsHttpFilter. Added XdsHttpFilter interface. Registry types updated to non-generic factories.
Remove ParsedFilterConfig
xds/src/main/java/com/linecorp/armeria/xds/ParsedFilterConfig.java
Deleted ParsedFilterConfig; callers now handle raw Any protobufs instead of pre-parsed configs.
FilterUtil & Retry Wiring
xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java
Switched merge/resolve APIs to Map<String, Any>, added resolveInstance(HttpFilter, @nullable Any) returning XdsHttpFilter or null for optional/unknown filters; retry wiring now accepts @Nullable RetryPolicy and builds retrying decorator when present.
Route/Entry Snapshot Changes
xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java, xds/src/main/java/com/linecorp/armeria/xds/RouteSnapshot.java, xds/src/main/java/com/linecorp/armeria/xds/VirtualHostSnapshot.java
Removed per-filter parsing/storage from snapshots. RouteEntry now stores Map<String, Any>; merging done in constructor; upstream filter construction consumes raw configs and simplified retry handling.
Stream Context Threading
xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java, xds/src/main/java/com/linecorp/armeria/xds/ListenerStream.java, xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java
RouteStream now accepts and propagates ListenerXdsResource to downstream streams; ListenerStream constructs RouteStream with listener context; ListenerSnapshot assigns routeSnapshot directly.
Router Filter Factory Update
xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java
Converted typed RouterFilterFactory to untyped factory; removed typed config APIs and returns a shared XdsHttpFilter instance irrespective of Any config.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • trustin
  • ikhoon
  • minwoox

Poem

🐇 I munched the ParsedConfig heap,
Any now hops where bytes once slept,
Factories craft filters on the fly,
Routes and listeners hold hands nearby,
Hooray — fresh tunnels for XDS delight! 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title partially relates to the changeset—it mentions handling unknown Message configs, which is one aspect of the changes, but omits the broader architectural refactoring (removal of ParsedFilterConfig, untyped HttpFilterFactory, per-route config merging in RouteEntry, etc.) that represents the main intent.
Description check ✅ Passed The description comprehensively covers the motivation, modifications, and breaking changes, directly addressing the PR's main objectives of replacing the typed HttpFilterFactory with an untyped version and moving config merging into RouteEntry.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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 (2)
xds/src/main/java/com/linecorp/armeria/xds/client/endpoint/RouterFilterFactory.java (2)

62-64: Parameter name should match interface contract.

The interface defines the parameter as httpFilter but the implementation uses filter. Aligning the names improves consistency and IDE navigation.

♻️ Suggested fix
     `@Override`
-    public XdsHttpFilter create(HttpFilter filter, Any config) {
+    public XdsHttpFilter create(HttpFilter httpFilter, Any config) {
         return routerFilter;
     }
🤖 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/client/endpoint/RouterFilterFactory.java`
around lines 62 - 64, The create method in RouterFilterFactory currently
declares its first parameter as "filter" but the interface contract uses
"httpFilter"; rename the parameter in the method signature to "httpFilter" to
match the interface (method: create(HttpFilter httpFilter, Any config) in class
RouterFilterFactory) so IDEs and callers resolve symbols consistently while
leaving the body (returning routerFilter) unchanged.

44-54: Consider making routerFilter a static field.

Since the anonymous XdsHttpFilter only references the static httpFilter and rpcFilter fields and has no instance state, it could be declared static for consistency with the other static fields and to avoid creating a new instance per factory.

♻️ Suggested refactor
-    private final XdsHttpFilter routerFilter = new XdsHttpFilter() {
+    private static final XdsHttpFilter ROUTER_FILTER = new XdsHttpFilter() {
         `@Override`
         public HttpPreprocessor httpPreprocessor() {
             return httpFilter::execute;
         }

         `@Override`
         public RpcPreprocessor rpcPreprocessor() {
             return rpcFilter::execute;
         }
     };

Then update the create method:

     `@Override`
     public XdsHttpFilter create(HttpFilter filter, Any config) {
-        return routerFilter;
+        return ROUTER_FILTER;
     }
🤖 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/client/endpoint/RouterFilterFactory.java`
around lines 44 - 54, The anonymous XdsHttpFilter assigned to routerFilter is
instance-specific but only references the static httpFilter and rpcFilter and
has no instance state; make routerFilter a static field (declare routerFilter as
static) and adjust any usages in the create method to reference the now-static
RouterFilterFactory.routerFilter symbol as needed (ensure httpPreprocessor() and
rpcPreprocessor() still return httpFilter::execute and rpcFilter::execute
respectively).
🤖 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/client/endpoint/RouterFilterFactory.java`:
- Around line 62-64: The create method in RouterFilterFactory currently declares
its first parameter as "filter" but the interface contract uses "httpFilter";
rename the parameter in the method signature to "httpFilter" to match the
interface (method: create(HttpFilter httpFilter, Any config) in class
RouterFilterFactory) so IDEs and callers resolve symbols consistently while
leaving the body (returning routerFilter) unchanged.
- Around line 44-54: The anonymous XdsHttpFilter assigned to routerFilter is
instance-specific but only references the static httpFilter and rpcFilter and
has no instance state; make routerFilter a static field (declare routerFilter as
static) and adjust any usages in the create method to reference the now-static
RouterFilterFactory.routerFilter symbol as needed (ensure httpPreprocessor() and
rpcPreprocessor() still return httpFilter::execute and rpcFilter::execute
respectively).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 87b17a2a-2092-429a-8ba1-b8187d05fea8

📥 Commits

Reviewing files that changed from the base of the PR and between 339b5dd and 51f4805.

📒 Files selected for processing (12)
  • xds/src/main/java/com/linecorp/armeria/xds/FilterUtil.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/ListenerStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/ParsedFilterConfig.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteEntry.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/RouteStream.java
  • xds/src/main/java/com/linecorp/armeria/xds/VirtualHostSnapshot.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/main/java/com/linecorp/armeria/xds/filter/XdsHttpFilter.java
💤 Files with no reviewable changes (2)
  • xds/src/main/java/com/linecorp/armeria/xds/VirtualHostSnapshot.java
  • xds/src/main/java/com/linecorp/armeria/xds/ParsedFilterConfig.java

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 70.73171% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.00%. Comparing base (8150425) to head (c3dd7c5).
⚠️ Report is 411 commits behind head on main.

Files with missing lines Patch % Lines
...main/java/com/linecorp/armeria/xds/FilterUtil.java 40.90% 10 Missing and 3 partials ⚠️
...main/java/com/linecorp/armeria/xds/RouteEntry.java 78.26% 3 Missing and 2 partials ⚠️
...com/linecorp/armeria/xds/filter/XdsHttpFilter.java 0.00% 4 Missing ⚠️
...ain/java/com/linecorp/armeria/xds/RouteStream.java 90.90% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6710      +/-   ##
============================================
- Coverage     74.46%   74.00%   -0.46%     
- Complexity    22234    24099    +1865     
============================================
  Files          1963     2175     +212     
  Lines         82437    90427    +7990     
  Branches      10764    11850    +1086     
============================================
+ Hits          61385    66924    +5539     
- Misses        15918    17888    +1970     
- Partials       5134     5615     +481     

☔ 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 changed the title Allow more flexibility to XdsFilter by handling unknown configuration Allow XdsFilter to handle unknown Message config Apr 7, 2026
@jrhee17 jrhee17 marked this pull request as ready for review April 8, 2026 01:28
Copy link
Copy Markdown
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

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

👍👍

if (!httpFilter.getIsOptional()) {
throw new IllegalArgumentException(
"Unknown HTTP filter '" + httpFilter.getName() +
"': no HttpFilterFactory registered. Register an SPI " +
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.

Question) I was wondering if it is technically possible to register a HttpFilterFactory via a builder API?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think it is difficult to do this, and I agree it will be useful for users.
I've simply deferred exposing builder methods to XdsBootstrapBuilder until the overall implementation is stable.

Copy link
Copy Markdown
Contributor

@minwoox minwoox left a comment

Choose a reason for hiding this comment

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

👍 👍 👍

* Returning {@code null} from {@link #create} causes the filter to be silently skipped.
*/
@UnstableApi
public interface HttpFilterFactory<T extends Message> {
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.

Should this be mentioned in the breaking change section?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the label and also added a description of the change to the PR description.

@jrhee17 jrhee17 merged commit 9ddbeeb into line:main Apr 9, 2026
15 of 17 checks passed
jrhee17 added a commit to jrhee17/armeria that referenced this pull request Apr 13, 2026
Subset of line#6700

Motivation

`HttpFilterFactory<T extends Message>` forced each factory
implementation to declare a specific protobuf `Message` type for its
config, and required the framework to own config parsing and
`FilterConfig` envelope unwrapping. This made it difficult to handle
unknown or dynamically-typed xDS filter configs (e.g. Istio-specific
filters), and leaked framework concerns into the factory API.

Additionally, `ParsedFilterConfig` wrapped per-route filter configs and
required the framework to merge and propagate them through the snapshot
hierarchy (`RouteSnapshot` → `VirtualHostSnapshot` → `RouteEntry`),
coupling snapshot construction to filter logic.

Modifications

- Replace `HttpFilterFactory<T extends Message>` with an untyped
`HttpFilterFactory` interface; `create(HttpFilter, Any)` now receives
the raw `Any` config directly, and factories are responsible for all
parsing including `FilterConfig` envelope unwrapping. Returning `null`
skips the filter.
- Introduce `XdsHttpFilter` as the return type of `create()`, with
default no-op `httpPreprocessor()`, `rpcPreprocessor()`,
`httpDecorator()`, and `rpcDecorator()` methods.
- Delete `ParsedFilterConfig`; per-route filter configs are now stored
and merged as `Map<String, Any>` directly.
- Move 3-level `typed_per_filter_config` merging (route-config → vhost →
route) into `RouteEntry`'s constructor, eliminating
`withFilterConfigs()` from `VirtualHostSnapshot` and `withRouter()` from
`RouteSnapshot`.
- Thread `@Nullable ListenerXdsResource` through `RouteStream` inner
classes so `RouteEntry` can access upstream HTTP filters from the Router
directly.
- Update `RouterFilterFactory` to implement the new untyped interface.
- Simplify `FilterUtil`: remove `toParsedFilterConfigs()`, update
`mergeFilterConfigs` to operate on `Map<String, Any>`,
`buildUpstreamFilter` takes `@Nullable RetryPolicy` directly.

Result

- `HttpFilterFactory` implementations no longer need to declare a
protobuf type parameter, and can handle any xDS filter config including
unknown or Istio-specific ones.
- Per-route filter config merging is self-contained in `RouteEntry`
rather than spread across the snapshot hierarchy.
- `RouteEntry.filterConfig(String)` now returns `@Nullable Any` instead
of `@Nullable ParsedFilterConfig`.

Breaking

- `HttpFilterFactory` implementations must now implement `XdsHttpFilter
create(HttpFilter httpFilter, Any config)` for custom filter
implementations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants