Skip to content

router: add filter_state internal redirect predicate#45904

Open
juanmolle wants to merge 8 commits into
envoyproxy:mainfrom
juanmolle:internal-redirect-filter-state-predicate
Open

router: add filter_state internal redirect predicate#45904
juanmolle wants to merge 8 commits into
envoyproxy:mainfrom
juanmolle:internal-redirect-filter-state-predicate

Conversation

@juanmolle

@juanmolle juanmolle commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Commit Message:
router: add filter_state internal redirect predicate

Adds the envoy.internal_redirect_predicates.filter_state predicate, which decides
whether to follow an internal redirect based on a boolean filter state object. The
predicate reads a BoolAccessor at the configured filter_state_key and accepts
the redirect if the value is true (or false if invert: true).

Also adds a generic envoy.bool filter state factory that accepts: true, t,
yes, y, 1 for true; false, f, no, n, 0 for false (case-insensitive).

Example configuration:

internal_redirect_policy:
  predicates:
    - name: envoy.internal_redirect_predicates.filter_state
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.internal_redirect.filter_state.v3.FilterStateConfig
        filter_state_key: envoy.internal_redirect.gate
        invert: false

Setting from Lua:

handle:streamInfo():filterState():set("envoy.internal_redirect.gate", "envoy.bool", "true")

Risk Level: Low — self-contained extension, inert unless explicitly configured.

Testing: Unit tests for predicate and factory. Integration tests with Docker validate
end-to-end behavior.

Docs Changes: Added envoy.bool factory to well_known_filter_state.rst.

Release Notes: Added changelogs/current/new_features/router__added-filter-state-internal-redirect-predicate.rst.

[Generative AI usage disclosure: Generative AI (Claude) was used to assist with this change;
I have reviewed and understand all of the code.]

Adds the envoy.internal_redirect_predicates.filter_state predicate, which
decides whether to follow an internal redirect by comparing a filter state
string object (set earlier in the request by another filter) against a
configured value: allow_value follows only on a match, deny_value follows
unless it matches, and allow_if_absent controls behavior when the object is
missing (default: do not follow). No change for existing configs; the
predicate has effect only when added to an InternalRedirectPolicy.

Uses a raw string with a required oneof allow_value / deny_value rather than a
StringMatcher, because the predicate factory does not receive a
CommonFactoryContext (needed by StringMatcher for regex/custom). This stays
additively upgradeable to a StringMatcher option later without an API break.

Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
@repokitteh-read-only

Copy link
Copy Markdown

As a reminder, PRs marked as draft will not be automatically assigned reviewers,
or be handled by maintainer-oncall triage.

Please mark your PR as ready when you want it to be reviewed!

🐱

Caused by: #45904 was opened by juanmolle.

see: more, trace.

Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
@juanmolle
juanmolle marked this pull request as ready for review June 30, 2026 22:00
@juanmolle
juanmolle requested a review from botengyao as a code owner June 30, 2026 22:00
@repokitteh-read-only

Copy link
Copy Markdown

CC @envoyproxy/api-shepherds: Your approval is needed for changes made to (api/envoy/|docs/root/api-docs/).
envoyproxy/api-shepherds assignee is @adisuissa
CC @envoyproxy/api-watchers: FYI only for changes made to (api/envoy/|docs/root/api-docs/).

🐱

Caused by: #45904 was ready_for_review by juanmolle.

see: more, trace.

@adisuissa adisuissa left a comment

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.

Thanks.
Left a high-level question about the design.

// whether an otherwise-enabled internal redirect is actually taken, without
// changing route matching.
//
// The predicate reads the filter-state object named ``filter_state_key`` (a

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.

From a high-level perspective:
it seems that in the future this will have a matcher that is dependent on the expected type. The most basic one will be a string matcher, but the API should be designed in a way that allows other types of matchers.

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 thought that using oneof will allow in the future to add others like string matcher with backward compatibility

oneof policy {
string allow_value = 2; // existing - exact string match
string deny_value = 3; // existing - exact string match
StringMatcher allow_string_matcher = 5; // NEW - regex, prefix, suffix, etc.
StringMatcher deny_string_matcher = 6; // NEW - regex, prefix, suffix, etc.
}

if you have something better in mind let me know I can change it

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.

IIUC, @adisuissa means that the type of data store in the filter state might be more than a string.

From an API point of view, the policy and the filter_state_key is coupled by the type of the filter state's type.

What happens if the filter state pointed by filter_state_key stores something other than string?

An alternative might be:

string filter_state_key = 1;
enum MatchAction {
  UNSPECIFIED = 0;
  ALLOW = 1;
  DENY = 2;
}
MatchAction match_action = 2;
oneof matcher {
  string string_value = 1;
  // other types of matcher for extension
}

With all the above said, isn't it simpler for this to just read a boolean from a filter state that dictates whether to follow the redirect? What's the use case that requires a string value to be compared against?

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 didn't see a boolean could work and perhaps it is cleaner
Do you have something like this in mind?

message FilterStateConfig {
    // Name of the filter-state boolean object. If true, follows the redirect.
    string redirect_enabled_key = 1 [(validate.rules).string = {min_len: 1}];

    // Whether to follow the redirect when the filter-state object is absent.
    // Defaults to false (do not redirect if the gate wasn't explicitly set).
    bool redirect_if_absent = 2;
}

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.

I think this is a lot cleaner while being really flexible - whichever filter producing the filter state can decide what to do.

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.

the code is cleaner too (and just a couple of lines) I will do some exploratory test and update the PR, thanks

@adisuissa

Copy link
Copy Markdown
Contributor

Assigning internal_redirect code-owners:
/assign @botengyao @penguingao

@penguingao penguingao left a comment

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.

Thanks for the contribution. This looks great. Just need to figure out the API discussion.

// whether an otherwise-enabled internal redirect is actually taken, without
// changing route matching.
//
// The predicate reads the filter-state object named ``filter_state_key`` (a

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.

IIUC, @adisuissa means that the type of data store in the filter state might be more than a string.

From an API point of view, the policy and the filter_state_key is coupled by the type of the filter state's type.

What happens if the filter state pointed by filter_state_key stores something other than string?

An alternative might be:

string filter_state_key = 1;
enum MatchAction {
  UNSPECIFIED = 0;
  ALLOW = 1;
  DENY = 2;
}
MatchAction match_action = 2;
oneof matcher {
  string string_value = 1;
  // other types of matcher for extension
}

With all the above said, isn't it simpler for this to just read a boolean from a filter state that dictates whether to follow the redirect? What's the use case that requires a string value to be compared against?

A fourth predicate :ref:`filter_state
<envoy_v3_api_msg_extensions.internal_redirect.filter_state.v3.FilterStateConfig>`
can be used to gate the redirect on a filter state value set earlier in the request by another
filter, allowing a per-request decision about whether an internal redirect is followed.

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.

looks like we just need it to be per-request, in which case, it's really just a boolean to be delivered.

const envoy::extensions::internal_redirect::filter_state::v3::FilterStateConfig&>(
config, ProtobufMessage::getStrictValidationVisitor());

using ProtoConfig = envoy::extensions::internal_redirect::filter_state::v3::FilterStateConfig;

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.

If you move this above line 19, you can use it with the call to downcastAndValidate as well.

EXPECT_EQ(predicate->name(), "envoy.internal_redirect_predicates.filter_state");
}

// allow_value: follow only when the gate equals the configured value.

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.

the tests here are not "config_test" - we might want to just rename this file to reflect the nature of this test file.

Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
@juanmolle
juanmolle force-pushed the internal-redirect-filter-state-predicate branch from 16a6b09 to 862c0d3 Compare July 13, 2026 17:00
Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
format_string:
text_format_source:
inline_string: "true"

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 know if there was a reason to not have this generic bool, I added it to be able to write bool directly in filter state, if this is not correct let me know and I will remove and store "true" as string in the filter state

@juanmolle

Copy link
Copy Markdown
Contributor Author

@penguingao @adisuissa If you have some time to take a look, I will appreciate

@penguingao penguingao left a comment

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.

Thanks! This looks great to me modulo a few nits.

@@ -0,0 +1,8 @@
Added :ref:`filter_state
<envoy_v3_api_msg_extensions.internal_redirect.filter_state.v3.FilterStateConfig>`,
a new internal redirect predicate that gates redirect decisions on a boolean filter-state

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.

nit: s/a new internal redirect/an internal redirect

this will not be new in some future point.

object set earlier in the request lifecycle (for example by a Lua filter, ext_proc,
``set_filter_state``, or a dynamic module). The predicate follows the redirect when the
boolean value is true, enabling per-request redirect control without changing route
matching. Also added the generic ``envoy.bool`` filter state factory for creating

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.

nit: "Also added ..." here belongs to release note instead of the static documentation.

std::unique_ptr<StreamInfo::FilterState::Object>
createFromBytes(absl::string_view data) const override {
bool value = false;
(void)absl::SimpleAtob(data, &value);

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.

I'd prefer we throw if this is false.

it's better to reject bad config than taking an assumption.

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.

true

}

private:
using ProtoConfig = envoy::extensions::internal_redirect::filter_state::v3::FilterStateConfig;

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.

is this still useful?

- changelog: use 'an' instead of 'a new', remove envoy.bool sentence
- GenericBoolObjectFactory: throw on invalid input instead of defaulting to false
- config.h: remove duplicate ProtoConfig typedef

Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
- changelog: use 'an' instead of 'a new', remove envoy.bool sentence
- GenericBoolObjectFactory: throw on invalid input instead of defaulting to false
- config.h: move ProtoConfig typedef to private class scope to avoid duplication

Signed-off-by: Juan Manuel Olle <jolle@salesforce.com>
@penguingao

Copy link
Copy Markdown
Contributor

This LGTM.

@botengyao for API approval and merge, unless @adisuissa has more feedbacks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants