Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
54e05f7
complete the development of transform
wbpcode Oct 18, 2025
2030088
Merge branch 'main' of ssh://ssh.github.com:443/envoyproxy/envoy into…
wbpcode Oct 18, 2025
a20c2c7
add a change log
wbpcode Oct 18, 2025
9ec7082
add more test and docs
wbpcode Oct 19, 2025
9a93058
fix format
wbpcode Oct 20, 2025
c64b2dd
Update api/envoy/extensions/filters/http/transform/v3/transform.proto
wbpcode Oct 21, 2025
21b6d72
Update api/envoy/extensions/filters/http/transform/v3/transform.proto
wbpcode Oct 21, 2025
b433bd2
Update api/envoy/extensions/filters/http/transform/v3/transform.proto
wbpcode Oct 21, 2025
e2118ad
address comments
wbpcode Oct 21, 2025
0ade6f0
Merge branch 'main' of https://github.com/envoyproxy/envoy into dev-b…
wbpcode Oct 21, 2025
d76c317
Merge branch 'main' of https://github.com/envoyproxy/envoy into dev-b…
wbpcode Oct 27, 2025
1229547
fix format
wbpcode Oct 27, 2025
3da5a18
Merge branch 'main' of https://github.com/envoyproxy/envoy into dev-b…
wbpcode Oct 28, 2025
e60bb8e
fix test
wbpcode Oct 28, 2025
319691e
Merge branch 'main' of https://github.com/envoyproxy/envoy into dev-b…
wbpcode Oct 29, 2025
8f80f33
Update api/envoy/extensions/filters/http/transform/v3/transform.proto
wbpcode Oct 29, 2025
daf5b62
Update api/envoy/extensions/filters/http/transform/v3/transform.proto
wbpcode Oct 29, 2025
eec33b2
Update api/envoy/extensions/filters/http/transform/v3/transform.proto
wbpcode Oct 29, 2025
f5e47b5
Update api/envoy/extensions/filters/http/transform/v3/transform.proto
wbpcode Oct 29, 2025
43dd986
more clear comments
wbpcode Oct 29, 2025
141f08f
Merge branch 'dev-body-transform' of https://github.com/wbpcode/envoy…
wbpcode Oct 29, 2025
b7cbebc
Merge branch 'main' of https://github.com/envoyproxy/envoy into dev-b…
wbpcode Nov 1, 2025
b4db80e
address the comments
wbpcode Nov 1, 2025
6655b4a
Merge branch 'main' of https://github.com/envoyproxy/envoy into dev-b…
wbpcode Nov 7, 2025
6c811be
remove unnecessary alias
wbpcode Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ extensions/upstreams/tcp @ggreenway @mattklein123
/*/extensions/string_matcher/ @ggreenway @cpakulski
# Header mutation
/*/extensions/filters/http/header_mutation @wbpcode @yanavlasov
# Body transform
/*/extensions/filters/http/transform @wbpcode @UNOWNED
# Health checkers
/*/extensions/health_checkers/grpc @zuercher @botengyao
/*/extensions/health_checkers/http @zuercher @botengyao
Expand Down
1 change: 1 addition & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ proto_library(
"//envoy/extensions/filters/http/stateful_session/v3:pkg",
"//envoy/extensions/filters/http/tap/v3:pkg",
"//envoy/extensions/filters/http/thrift_to_metadata/v3:pkg",
"//envoy/extensions/filters/http/transform/v3:pkg",
"//envoy/extensions/filters/http/upstream_codec/v3:pkg",
"//envoy/extensions/filters/http/wasm/v3:pkg",
"//envoy/extensions/filters/listener/http_inspector/v3:pkg",
Expand Down
13 changes: 13 additions & 0 deletions api/envoy/extensions/filters/http/transform/v3/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py.

load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")

licenses(["notice"]) # Apache 2

api_proto_package(
deps = [
"//envoy/config/common/mutation_rules/v3:pkg",
"//envoy/config/core/v3:pkg",
"@com_github_cncf_xds//udpa/annotations:pkg",
],
)
99 changes: 99 additions & 0 deletions api/envoy/extensions/filters/http/transform/v3/transform.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
syntax = "proto3";

package envoy.extensions.filters.http.transform.v3;

import "envoy/config/common/mutation_rules/v3/mutation_rules.proto";
import "envoy/config/core/v3/substitution_format_string.proto";

import "udpa/annotations/status.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.extensions.filters.http.transform.v3";
option java_outer_classname = "TransformProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/transform/v3;transformv3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: Transform filter configuration]
// Transform filter :ref:`configuration overview <config_http_filters_transform>` to perform
// HTTP header and body transformations.
// [#extension: envoy.filters.http.transform]

// Configuration for the transform filter. The filter may buffer the request/response until the
// entire body is received, and then mutate the headers and body according to the contents
// of the request/response. The request and response transformations are independent and could
// be configured separately.
// Only JSON body transformation is supported for now.
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if there should just be a JsonTransform filter here, instead of something generic. The reason I bring this up is because the transformations seem to be highly coupled with JSON bodies. (this is a discussion and not a a call for action).
WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's actually depends the feedback from the users and community, I personally hope this filter could handle various content type like plain text or url form by extend other fields in this filter.
JSON is supported first is because that the JSON is used widely for now in open API and AI related things. In addition, for the response, we need to support event stream to support AI traffic.

message TransformConfig {
// Configuration for transforming request.
//
// .. note::
//
// If set then the entire request headers and body will always be buffered on a JSON request
// even if only headers are transformed.
Transformation request_transformation = 1;

// Configuration for transforming response.
//
// .. note::
//
// If set then the entire response headers and body will always be buffered on a JSON response
// even if only headers are transformed.
Transformation response_transformation = 2;

// If true and the request headers are transformed, Envoy will re-evaluate the target
// cluster in the same route. Please ensure the cluster specifier in the route supports
// dynamic evaluation or this flag will have no effect, e.g.
// :ref:`matcher cluster specifier
// <envoy_v3_api_msg_extensions.router.cluster_specifiers.matcher.v3.MatcherClusterSpecifier>`.
//
// Only one of ``clear_cluster_cache`` and ``clear_route_cache`` can be true.
bool clear_cluster_cache = 3;

// If true and the request headers are transformed, Envoy will clear the route cache for
// the current request and force re-evaluation of the route. This has performance penalty and
// should only be used when the route match criteria depends on the transformed headers.
//
// Only one of ``clear_cluster_cache`` and ``clear_route_cache`` can be true.
bool clear_route_cache = 4;
}

message Transformation {
// The header mutations to perform.
// The :ref:`substitution format specifier <config_access_log_format>` could be applied here.
// In addition to the commonly used format specifiers, this filter introduces additional format specifiers:
//
// * ``%REQUEST_BODY(KEY*)%``: the request body. And ``Key`` KEY is an optional
// lookup key in the namespace with the option of specifying nested keys separated by ':'.
// * ``%RESPONSE_BODY(KEY*)%``: the response body. And ``Key`` KEY is an optional
// lookup key in the namespace with the option of specifying nested keys separated by ':'.
repeated config.common.mutation_rules.v3.HeaderMutation headers_mutations = 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Will the headers be blocked until the body is buffered even if headers_mutations isn't configured?

Copy link
Member Author

@wbpcode wbpcode Oct 29, 2025

Choose a reason for hiding this comment

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

For now, yes for simplification of the code. But this is good point that could be optimized.


Hmmm, it will require the decodeData to handle the continue or stop of decodeHeaders correctly. Seems it's not deserved to optimize because the additional complexity 🤔 The filter will not be performant anyway because the body processing.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that this is an implementation issue, but I think it should be spelled clearly in a note above (example: buffering of the entire request headers and body will always happen on a request even if only headers are transformed).

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure.


// The body transformation configuration. If not set, no body transformation will be performed.
BodyTransformation body_transformation = 2;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you see a case for multiple body transformations?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope. This is a one time logic. We will generate a new body based on the body_format and then the new body will replace or be merge into original body content.
If multiple phase transformations is necessary, the users could configure multiple transform filters in the filter chain anyway. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess the same can be said about headers, but I agree that headers are more likely to have multiple transformations (one for each header key).

}

message BodyTransformation {
enum TransformAction {
// Merge the transformed body with the original body. This is the default action.
MERGE = 0;

// Replace the original body with the transformed body.
REPLACE = 1;
}

// Body transformation configuration. The substitution format string is used as the template
// to generate the transformed new body content.
// The :ref:`substitution format specifier <config_access_log_format>` could be applied here.
// And except the commonly used format specifiers, the additional format specifiers
// ``%REQUEST_BODY(KEY*)%`` and ``%RESPONSE_BODY(KEY*)%`` could also be used here.
config.core.v3.SubstitutionFormatString body_format = 1
[(validate.rules).message = {required: true}];

// The action to perform for new body content and original body content.
// For example, if ``MERGE`` is used, then the new body content generated from the ``body_format``
// will be merged into the original body content.
//
// Default is ``MERGE``.
TransformAction action = 2;
}
1 change: 1 addition & 0 deletions api/versioning/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ proto_library(
"//envoy/extensions/filters/http/stateful_session/v3:pkg",
"//envoy/extensions/filters/http/tap/v3:pkg",
"//envoy/extensions/filters/http/thrift_to_metadata/v3:pkg",
"//envoy/extensions/filters/http/transform/v3:pkg",
"//envoy/extensions/filters/http/upstream_codec/v3:pkg",
"//envoy/extensions/filters/http/wasm/v3:pkg",
"//envoy/extensions/filters/listener/http_inspector/v3:pkg",
Expand Down
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ removed_config_or_runtime:
Removed runtime guard ``envoy.reloadable_features.report_load_with_rq_issued`` and legacy code paths.

new_features:
- area: http filter
change: |
Added :ref:`transform http filter <config_http_filters_transform>` which adds the ability to modify request
and response bodies in any position of HTTP filter chain.
This also make it possible to refresh routes based on the attributes in the request body.
- area: access_log
change: |
Support process-level rate limiting on access log emission by
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains:
- "*"
routes:
- match:
prefix: "/route/override"
route:
host_rewrite_literal: upstream.com
cluster: upstream_com
typed_per_filter_config:
transform:
"@type": type.googleapis.com/envoy.extensions.filters.http.transform.v3.TransformConfig
request_transformation:
headers_mutations:
- append:
header:
key: "model-header"
value: "%REQUEST_BODY(model)%"
append_action: OVERWRITE_IF_EXISTS_OR_ADD
- match:
prefix: "/"
route:
host_rewrite_literal: upstream.com
cluster: upstream_com
http_filters:
- name: transform
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.transform.v3.TransformConfig
request_transformation:
headers_mutations:
- append:
header:
key: "model-header"
value: "%REQUEST_BODY(model)%"
append_action: OVERWRITE_IF_EXISTS_OR_ADD
body_transformation:
body_format:
json_format:
model: "new-model"
action: MERGE
response_transformation:
headers_mutations:
- append:
header:
key: "prompt-tokens"
value: "%RESPONSE_BODY(usage:prompt_tokens)%"
append_action: OVERWRITE_IF_EXISTS_OR_ADD
- append:
header:
key: "completion-tokens"
value: "%RESPONSE_BODY(usage:completion_tokens)%"
append_action: OVERWRITE_IF_EXISTS_OR_ADD
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

clusters:
- name: upstream_com
type: LOGICAL_DNS
# Comment out the following line to test on v6 networks
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: service_upstream_com
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: upstream.com
port_value: 443
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: upstream.com
1 change: 1 addition & 0 deletions docs/root/configuration/http/http_filters/http_filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ HTTP filters
thrift_to_metadata_filter
upstream_codec_filter
wasm_filter
transform_filter
60 changes: 60 additions & 0 deletions docs/root/configuration/http/http_filters/transform_filter.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
.. _config_http_filters_transform:

Transform
=========

* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.transform.v3.TransformConfig``.
* :ref:`v3 API reference <envoy_v3_api_msg_extensions.filters.http.transform.v3.TransformConfig>`

This filter can be used to transform HTTP requests and responses with
:ref:`substitution format string <envoy_v3_api_msg_config.core.v3.SubstitutionFormatString>`. For example, it can be used to:

* Modify request or response headers based on values in the body and refresh the routes accordingly.
* Modify JSON request or response bodies.

Configuration
-------------

The following example configuration will extract the ``model`` field from a JSON request body
and add it as a request header ``model-header`` before forwarding the request to the upstream.
At the same time, it will also rewrite the ``model`` field in the JSON response body as ``new-model``.

At the response path, the filter is configured to extract the ``completion_tokens`` and ``prompt_tokens``
fields from the JSON response body and add them as response headers.

.. literalinclude:: _include/transform_filter.yaml
:language: yaml
:lines: 42-69
:lineno-start: 42
:linenos:
:caption: :download:`transform_filter.yaml <_include/transform_filter.yaml>`

Per-route configuration
-----------------------

Per-route overrides may be supplied via the same protobuf API in the ``typed_per_filter_config``
field of route configuration.

The following example configuration will override the global filter configuration to keep
only the request headers transformation.

.. literalinclude:: _include/transform_filter.yaml
:language: yaml
:lines: 26-35
:lineno-start: 26
:linenos:
:caption: :download:`transform_filter.yaml <_include/transform_filter.yaml>`

Enhanced substitution format
----------------------------

The :ref:`substitution format specifier <config_access_log_format>` could be used for both
headers and body transformations.

And except the commonly used format specifiers, there are some additional format specifiers
provided by the transform filter:

* ``%REQUEST_BODY(KEY*)%``: the request body. And ``Key`` KEY is an optional
lookup key in the namespace with the option of specifying nested keys separated by ':'.
* ``%RESPONSE_BODY(KEY*)%``: the response body. And ``Key`` KEY is an optional
lookup key in the namespace with the option of specifying nested keys separated by ':'.
7 changes: 6 additions & 1 deletion source/common/config/metadata.cc
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ const Protobuf::Value& Metadata::metadataValue(const envoy::config::core::v3::Me
if (filter_it == metadata->filter_metadata().end()) {
return Protobuf::Value::default_instance();
}
const Protobuf::Struct* data_struct = &(filter_it->second);
return structValue(filter_it->second, path);
}

const Protobuf::Value& Metadata::structValue(const Protobuf::Struct& struct_value,
const std::vector<std::string>& path) {
const Protobuf::Struct* data_struct = &struct_value;
const Protobuf::Value* val = nullptr;
// go through path to select sub entries
for (const auto& p : path) {
Expand Down
9 changes: 9 additions & 0 deletions source/common/config/metadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ struct MetadataKey {
*/
class Metadata {
public:
/**
* Lookup value by a multi-key path in a Struct. If path is empty will return the entire struct.
* @param struct_value reference.
* @param path multi-key path.
* @return const Protobuf::Value& value if found, empty if not found.
*/
static const Protobuf::Value& structValue(const Protobuf::Struct& struct_value,
const std::vector<std::string>& path);

/**
* Lookup value of a key for a given filter in Metadata.
* @param metadata reference.
Expand Down
14 changes: 9 additions & 5 deletions source/common/http/header_mutation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ using HeaderValueOption = envoy::config::core::v3::HeaderValueOption;
// to reuse the formatter after the router's formatter is completely removed.
class AppendMutation : public HeaderEvaluator, public Envoy::Router::HeadersToAddEntry {
public:
AppendMutation(const HeaderValueOption& header_value_option, absl::Status& creation_status)
: HeadersToAddEntry(header_value_option, creation_status),
AppendMutation(const HeaderValueOption& header_value_option,
const Formatter::CommandParserPtrVector& command_parsers,
absl::Status& creation_status)
: HeadersToAddEntry(header_value_option, command_parsers, creation_status),
header_name_(header_value_option.header().key()) {}

void evaluateHeaders(Http::HeaderMap& headers, const Formatter::Context& context,
Expand Down Expand Up @@ -88,22 +90,24 @@ class RemoveOnMatchMutation : public HeaderEvaluator {

absl::StatusOr<std::unique_ptr<HeaderMutations>>
HeaderMutations::create(const ProtoHeaderMutatons& header_mutations,
Server::Configuration::CommonFactoryContext& context) {
Server::Configuration::CommonFactoryContext& context,
const Formatter::CommandParserPtrVector& command_parsers) {
absl::Status creation_status = absl::OkStatus();
auto ret = std::unique_ptr<HeaderMutations>(
new HeaderMutations(header_mutations, context, creation_status));
new HeaderMutations(header_mutations, context, command_parsers, creation_status));
RETURN_IF_NOT_OK(creation_status);
return ret;
}

HeaderMutations::HeaderMutations(const ProtoHeaderMutatons& header_mutations,
Server::Configuration::CommonFactoryContext& context,
const Formatter::CommandParserPtrVector& command_parsers,
absl::Status& creation_status) {
for (const auto& mutation : header_mutations) {
switch (mutation.action_case()) {
case envoy::config::common::mutation_rules::v3::HeaderMutation::ActionCase::kAppend:
header_mutations_.emplace_back(
std::make_unique<AppendMutation>(mutation.append(), creation_status));
std::make_unique<AppendMutation>(mutation.append(), command_parsers, creation_status));
if (!creation_status.ok()) {
return;
}
Expand Down
Loading
Loading