diff --git a/source/extensions/filters/http/ext_proc/mutation_utils.cc b/source/extensions/filters/http/ext_proc/mutation_utils.cc index d56cd20c2fb78..b0a9787df8b76 100644 --- a/source/extensions/filters/http/ext_proc/mutation_utils.cc +++ b/source/extensions/filters/http/ext_proc/mutation_utils.cc @@ -24,6 +24,9 @@ using Stats::Counter; using envoy::service::ext_proc::v3::BodyMutation; using envoy::service::ext_proc::v3::HeaderMutation; +using HeaderAppendAction = envoy::config::core::v3::HeaderValueOption::HeaderAppendAction; +using HeaderValueOption = envoy::config::core::v3::HeaderValueOption; + bool MutationUtils::headerInMatcher( absl::string_view key, const std::vector& header_matchers) { return std::any_of(header_matchers.begin(), header_matchers.end(), @@ -146,6 +149,10 @@ absl::Status MutationUtils::applyHeaderMutations(const HeaderMutation& mutation, } } + bool append_mode_for_append_action; + Filters::Common::MutationRules::CheckOperation check_op_for_append_action; + Filters::Common::MutationRules::CheckResult checkResult_for_append_action; + for (const auto& sh : mutation.set_headers()) { if (!sh.has_header()) { continue; @@ -174,33 +181,74 @@ absl::Status MutationUtils::applyHeaderMutations(const HeaderMutation& mutation, return absl::InvalidArgumentError("Invalid character in set_headers mutation."); } const LowerCaseString header_name(sh.header().key()); - const bool append = PROTOBUF_GET_WRAPPED_OR_DEFAULT(sh, append, false); - const auto check_op = (append && !headers.get(header_name).empty()) ? CheckOperation::APPEND - : CheckOperation::SET; - auto check_result = checker.check(check_op, header_name, header_value); - if (replacing_message && header_name == Http::Headers::get().Method) { - // Special handling to allow changing ":method" when the - // CONTINUE_AND_REPLACE option is selected, to stay compatible. - check_result = CheckResult::OK; - } - switch (check_result) { - case CheckResult::OK: - ENVOY_LOG(trace, "Setting header {} append = {}", sh.header().key(), append); - if (append) { - headers.addCopy(header_name, header_value); - } else { - headers.setCopy(header_name, header_value); + + bool append = false; + if (sh.has_append()) { + append = sh.append().value(); + const auto check_op = (append && !headers.get(header_name).empty()) ? CheckOperation::APPEND + : CheckOperation::SET; + auto checkResult = handleCheckResult(headers, replacing_message, checker, rejected_mutations, + check_op, header_name, header_value, append); + if (checkResult == CheckResult::FAIL) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid attempt to modify ", static_cast(header_name))); + } + } else { + switch (sh.append_action()) { + case HeaderValueOption::APPEND_IF_EXISTS_OR_ADD: + append_mode_for_append_action = true; + check_op_for_append_action = + (!headers.get(header_name).empty()) ? CheckOperation::APPEND : CheckOperation::SET; + checkResult_for_append_action = handleCheckResult( + headers, replacing_message, checker, rejected_mutations, check_op_for_append_action, + header_name, header_value, append_mode_for_append_action); + if (checkResult_for_append_action == CheckResult::FAIL) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid attempt to modify ", static_cast(header_name))); + } + break; + case HeaderValueOption::ADD_IF_ABSENT: + if (headers.get(header_name).empty()) { + append_mode_for_append_action = true; + check_op_for_append_action = CheckOperation::SET; + checkResult_for_append_action = handleCheckResult( + headers, replacing_message, checker, rejected_mutations, check_op_for_append_action, + header_name, header_value, append_mode_for_append_action); + if (checkResult_for_append_action == CheckResult::FAIL) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid attempt to modify ", static_cast(header_name))); + } + } + break; + case HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD: + append_mode_for_append_action = false; + check_op_for_append_action = CheckOperation::SET; + checkResult_for_append_action = handleCheckResult( + headers, replacing_message, checker, rejected_mutations, check_op_for_append_action, + header_name, header_value, append_mode_for_append_action); + if (checkResult_for_append_action == CheckResult::FAIL) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid attempt to modify ", static_cast(header_name))); + } + break; + case HeaderValueOption::OVERWRITE_IF_EXISTS: + if (!headers.get(header_name).empty()) { + append_mode_for_append_action = false; + check_op_for_append_action = CheckOperation::SET; + checkResult_for_append_action = handleCheckResult( + headers, replacing_message, checker, rejected_mutations, check_op_for_append_action, + header_name, header_value, append_mode_for_append_action); + if (checkResult_for_append_action == CheckResult::FAIL) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid attempt to modify ", static_cast(header_name))); + } + } + break; + default: + // Handle unknown/invalid append_action value + return absl::InvalidArgumentError(absl::StrCat( + "Invalid append_action value ", static_cast(header_name))); } - break; - case CheckResult::IGNORE: - ENVOY_LOG(debug, "Header {} may not be modified per rules", header_name); - rejected_mutations.inc(); - break; - case CheckResult::FAIL: - ENVOY_LOG(debug, "Header {} may not be modified. Returning error", header_name); - rejected_mutations.inc(); - return absl::InvalidArgumentError( - absl::StrCat("Invalid attempt to modify ", static_cast(header_name))); } } @@ -208,6 +256,39 @@ absl::Status MutationUtils::applyHeaderMutations(const HeaderMutation& mutation, return headerMutationResultCheck(headers, rejected_mutations); } +// Define a common function to handle the check result logic +Filters::Common::MutationRules::CheckResult MutationUtils::handleCheckResult( + Http::HeaderMap& headers, bool replacing_message, + const Filters::Common::MutationRules::Checker& checker, Stats::Counter& rejected_mutations, + Filters::Common::MutationRules::CheckOperation check_op, Http::LowerCaseString header_name, + absl::string_view header_value, bool append_mode) { + auto check_result = checker.check(check_op, header_name, header_value); + if (replacing_message && header_name == Http::Headers::get().Method) { + // Special handling to allow changing ":method" when the + // CONTINUE_AND_REPLACE option is selected, to stay compatible. + check_result = CheckResult::OK; + } + // Print the value as an integer + switch (check_result) { + case CheckResult::OK: + if (append_mode) { + headers.addCopy(header_name, header_value); + } else { + headers.setCopy(header_name, header_value); + } + break; + case CheckResult::IGNORE: + rejected_mutations.inc(); + break; + case CheckResult::FAIL: + rejected_mutations.inc(); + // You can add additional handling for the FAIL case here if needed. + break; + } + + return check_result; +} + void MutationUtils::applyBodyMutations(const BodyMutation& mutation, Buffer::Instance& buffer) { switch (mutation.mutation_case()) { case BodyMutation::MutationCase::kClearBody: diff --git a/source/extensions/filters/http/ext_proc/mutation_utils.h b/source/extensions/filters/http/ext_proc/mutation_utils.h index bc5c1e7c00d74..795cca95d95cf 100644 --- a/source/extensions/filters/http/ext_proc/mutation_utils.h +++ b/source/extensions/filters/http/ext_proc/mutation_utils.h @@ -58,6 +58,12 @@ class MutationUtils : public Logger::Loggable { // Check whether the header size after mutation is over the HCM size config. static absl::Status headerMutationResultCheck(const Http::HeaderMap& headers, Stats::Counter& rejected_mutations); + + static Filters::Common::MutationRules::CheckResult handleCheckResult( + Http::HeaderMap& headers, bool replacing_message, + const Filters::Common::MutationRules::Checker& checker, Stats::Counter& rejected_mutations, + Filters::Common::MutationRules::CheckOperation check_op, Http::LowerCaseString header_name, + absl::string_view header_value, bool append_mode); }; } // namespace ExternalProcessing diff --git a/test/extensions/filters/http/ext_proc/filter_test.cc b/test/extensions/filters/http/ext_proc/filter_test.cc index 77d0479c3dd82..67553a3c035fd 100644 --- a/test/extensions/filters/http/ext_proc/filter_test.cc +++ b/test/extensions/filters/http/ext_proc/filter_test.cc @@ -3293,6 +3293,103 @@ TEST_F(HttpFilter2Test, LastEncodeDataCallExceedsStreamBufferLimitWouldJustRaise conn_manager_->onData(fake_input, false); } +TEST_F(HttpFilterTest, AppendActionTest) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({ + {"envoy.reloadable_features.send_header_raw_value", "false"}, + {"envoy.reloadable_features.header_value_option_change_action", "true"}, + }); + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + // Initialize request headers. + request_headers_.addCopy(LowerCaseString("x-original-header"), "original_value"); + + EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_headers_, false)); + + // Apply the "append_action" to add a value to an existing header. + processRequestHeaders( + false, [](const HttpHeaders&, ProcessingResponse&, HeadersResponse& header_resp) { + auto headers_mut = header_resp.mutable_response()->mutable_header_mutation(); + auto s = headers_mut->add_set_headers(); + s->mutable_header()->set_key("x-original-header"); + s->mutable_header()->set_value("appended_value"); + s->set_append_action(::envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_APPEND_IF_EXISTS_OR_ADD); + s = headers_mut->add_set_headers(); + s->mutable_header()->set_key("another-request"); + s->mutable_header()->set_value("request"); + s->set_append_action(::envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS_OR_ADD); + }); + + // Check that the header was appended correctly. + TestRequestHeaderMapImpl expected{{":path", "/"}, + {":method", "POST"}, + {":scheme", "http"}, + {"host", "host"}, + {"x-original-header", "original_value"}, + {"x-original-header", "appended_value"}, + {"another-request", "request"}}; + EXPECT_THAT(&request_headers_, HeaderMapEqualIgnoreOrder(&expected)); + + // Continue processing data and trailers. + Buffer::OwnedImpl req_data("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(req_data, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + // Initialize response headers. + response_headers_.addCopy(LowerCaseString(":status"), "200"); + response_headers_.addCopy(LowerCaseString("content-type"), "text/plain"); + response_headers_.addCopy(LowerCaseString("content-length"), "3"); + + EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->encodeHeaders(response_headers_, false)); + + // Apply the "append_action" to response headers. + processResponseHeaders(false, [](const HttpHeaders& response_headers, ProcessingResponse&, + HeadersResponse& header_resp) { + EXPECT_FALSE(response_headers.end_of_stream()); + TestRequestHeaderMapImpl expected_response{ + {":status", "200"}, {"content-type", "text/plain"}, {"content-length", "3"}}; + EXPECT_THAT(response_headers.headers(), HeaderProtosEqual(expected_response)); + + auto* resp_headers_mut = header_resp.mutable_response()->mutable_header_mutation(); + auto* s = resp_headers_mut->add_set_headers(); + s->mutable_header()->set_key("x-original-response-header"); + s->mutable_header()->set_value("appended_response_value"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_ADD_IF_ABSENT); + s = resp_headers_mut->add_set_headers(); + s->mutable_header()->set_key("x-original-response-header"); + s->mutable_header()->set_value("second value"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS); + }); + + // Check that the response header was appended correctly. + TestRequestHeaderMapImpl final_expected_response{{":status", "200"}, + {"content-type", "text/plain"}, + {"content-length", "3"}, + {"x-original-response-header", "second value"}}; + EXPECT_THAT(&response_headers_, HeaderMapEqualIgnoreOrder(&final_expected_response)); + + // Continue processing data, encode trailers, and destroy the filter. + Buffer::OwnedImpl resp_data("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(resp_data, false)); + Buffer::OwnedImpl empty_data; + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(empty_data, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(2, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(2, config_->stats().stream_msgs_received_.value()); + EXPECT_EQ(1, config_->stats().streams_closed_.value()); +} + } // namespace } // namespace ExternalProcessing } // namespace HttpFilters diff --git a/test/extensions/filters/http/ext_proc/mutation_utils_test.cc b/test/extensions/filters/http/ext_proc/mutation_utils_test.cc index b193e10e84c36..f47a697c4827b 100644 --- a/test/extensions/filters/http/ext_proc/mutation_utils_test.cc +++ b/test/extensions/filters/http/ext_proc/mutation_utils_test.cc @@ -130,6 +130,10 @@ TEST(MutationUtils, TestApplyMutations) { s = mutation.add_set_headers(); s->mutable_header()->set_key(":status"); s->mutable_header()->set_value("100"); + s = mutation.add_set_headers(); + s->mutable_append()->set_value(true); + s->mutable_header()->set_key("x-append-this"); + s->mutable_header()->set_value("1"); // Use the default mutation rules Checker checker(HeaderMutationRules::default_instance()); @@ -153,6 +157,7 @@ TEST(MutationUtils, TestApplyMutations) { {"x-remove-and-append-this", "4"}, {"x-replace-this", "nope"}, {"x-envoy-strange-thing", "No"}, + {"x-append-this", "1"}, }; EXPECT_THAT(&headers, HeaderMapEqualIgnoreOrder(&expected_headers)); @@ -412,6 +417,269 @@ TEST(MutationUtils, TestDisallowHeaderSetNotAllowHeader) { EXPECT_THAT(proto_headers, HeaderProtosEqual(expected)); } +TEST(MutationUtils, TestAppendActionAppendIfExistsOrAdd) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({ + {"envoy.reloadable_features.send_header_raw_value", "false"}, + }); + Http::TestResponseHeaderMapImpl headers{ + {"Set-Cookie", "Value123"}, + }; + + envoy::service::ext_proc::v3::HeaderMutation mutation; + auto* s = mutation.add_set_headers(); + s->mutable_header()->set_key("Set-Cookie"); + s->mutable_header()->set_value("Value234"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_APPEND_IF_EXISTS_OR_ADD); + s = mutation.add_set_headers(); + s->mutable_header()->set_key("Set-Cookie"); + s->mutable_header()->set_value("Value123"); + s->set_append_action(::envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_APPEND_IF_EXISTS_OR_ADD); + s = mutation.add_set_headers(); + s->mutable_header()->set_key("other-header"); + s->mutable_header()->set_value("xyz"); + s->set_append_action(::envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_APPEND_IF_EXISTS_OR_ADD); + // Attempts to set the status header out of range should + // also be ignored. + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":status"); + s->mutable_header()->set_value("This is not even an integer"); + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":status"); + s->mutable_header()->set_value("100"); + + Checker checker(HeaderMutationRules::default_instance()); + Envoy::Stats::MockCounter rejections; + EXPECT_CALL(rejections, inc()).Times(2); + + EXPECT_TRUE( + MutationUtils::applyHeaderMutations(mutation, headers, false, checker, rejections).ok()); + + Http::TestResponseHeaderMapImpl expected_headers{ + {"Set-Cookie", "Value123"}, + {"Set-Cookie", "Value234"}, + {"other-header", "xyz"}, + }; + + EXPECT_THAT(&headers, HeaderMapEqualIgnoreOrder(&expected_headers)); +} + +TEST(MutationUtils, TestAppendActionAddIfAbsent) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.send_header_raw_value", "false"}}); + Http::TestRequestHeaderMapImpl headers{ + {"Set-Cookie", "Value123"}, + }; + + envoy::service::ext_proc::v3::HeaderMutation mutation; + auto* s = mutation.add_set_headers(); + s->mutable_header()->set_key("Set-Cookie"); + s->mutable_header()->set_value("Value234"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_ADD_IF_ABSENT); + s = mutation.add_set_headers(); + s->mutable_header()->set_key("Set-Cookie"); + s->mutable_header()->set_value("Value123"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_ADD_IF_ABSENT); + s = mutation.add_set_headers(); + s->mutable_header()->set_key("other-header"); + s->mutable_header()->set_value("Value123"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_ADD_IF_ABSENT); + // Attempts to set the status header out of range should + // also be ignored. + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":status"); + s->mutable_header()->set_value("This is not even an integer"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_ADD_IF_ABSENT); + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":status"); + s->mutable_header()->set_value("100"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_ADD_IF_ABSENT); + + Checker checker(HeaderMutationRules::default_instance()); + Envoy::Stats::MockCounter rejections; + EXPECT_CALL(rejections, inc()).Times(2); + + EXPECT_TRUE( + MutationUtils::applyHeaderMutations(mutation, headers, false, checker, rejections).ok()); + + Http::TestRequestHeaderMapImpl expected_headers{{"Set-cookie", "Value123"}, + {"other-header", "Value123"}}; + + EXPECT_THAT(&headers, HeaderMapEqualIgnoreOrder(&expected_headers)); +} + +TEST(MutationUtils, TestAppendActionOverwriteOrAdd) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({{"envoy.reloadable_features.send_header_raw_value", "false"}}); + Http::TestRequestHeaderMapImpl headers{ + {"Set-Cookie", "Value123"}, + }; + + envoy::service::ext_proc::v3::HeaderMutation mutation; + auto* s = mutation.add_set_headers(); + s->mutable_header()->set_key("Set-Cookie"); + s->mutable_header()->set_value("Value234"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS_OR_ADD); + s = mutation.add_set_headers(); + s->mutable_header()->set_key("Set-Cookie"); + s->mutable_header()->set_value("Value123"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS_OR_ADD); + + s = mutation.add_set_headers(); + s->mutable_header()->set_key("other-header"); + s->mutable_header()->set_value("new value"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS_OR_ADD); + // Attempts to set the status header out of range should + // also be ignored. + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":status"); + s->mutable_header()->set_value("This is not even an integer"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS_OR_ADD); + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":status"); + s->mutable_header()->set_value("100"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS_OR_ADD); + + Checker checker(HeaderMutationRules::default_instance()); + Envoy::Stats::MockCounter rejections; + EXPECT_CALL(rejections, inc()).Times(2); + + EXPECT_TRUE( + MutationUtils::applyHeaderMutations(mutation, headers, false, checker, rejections).ok()); + + Http::TestRequestHeaderMapImpl expected_headers{{"Set-Cookie", "Value123"}, + {"other-header", "new value"}}; + + EXPECT_THAT(&headers, HeaderMapEqualIgnoreOrder(&expected_headers)); +} + +TEST(MutationUtils, TestAppendActionOverwriteIfExists) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({ + {"envoy.reloadable_features.send_header_raw_value", "false"}, + }); + Http::TestRequestHeaderMapImpl headers{ + {"Set-Cookie", "Value123"}, + {":status", "500"}, + }; + + envoy::service::ext_proc::v3::HeaderMutation mutation; + auto* s = mutation.add_set_headers(); + s->mutable_header()->set_key("Set-Cookie"); + s->mutable_header()->set_value("Value234"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS); + + s = mutation.add_set_headers(); + s->mutable_header()->set_key("other-header"); + s->mutable_header()->set_value("new value"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS); + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":status"); + s->mutable_header()->set_value("This is not even an integer"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS); + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":status"); + s->mutable_header()->set_value("100"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS); + Checker checker(HeaderMutationRules::default_instance()); + Envoy::Stats::MockCounter rejections; + EXPECT_CALL(rejections, inc()).Times(2); + + EXPECT_TRUE( + MutationUtils::applyHeaderMutations(mutation, headers, false, checker, rejections).ok()); + + Http::TestRequestHeaderMapImpl expected_headers{ + {"set-cookie", "Value234"}, + {":status", "500"}, + }; + + EXPECT_THAT(&headers, HeaderMapEqualIgnoreOrder(&expected_headers)); +} + +class CheckFailureTest : public testing::TestWithParam {}; + +INSTANTIATE_TEST_SUITE_P(Params, CheckFailureTest, testing::Values(4, 3, 2, 1, 0)); + +TEST_P(CheckFailureTest, TestApplyMutationsWithCheckFailure) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues({ + {"envoy.reloadable_features.send_header_raw_value", "false"}, + }); + + Http::TestRequestHeaderMapImpl headers{ + {"orignial-header", "any value"}, + }; + + envoy::service::ext_proc::v3::HeaderMutation mutation; + auto* s = mutation.add_set_headers(); + switch (GetParam()) { + case 0: + s->mutable_header()->set_key("x-check-this-header"); + s->mutable_header()->set_value("value-to-check"); + s->set_append_action(::envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_APPEND_IF_EXISTS_OR_ADD); + break; + case 1: + s->mutable_header()->set_key("x-check-this-header"); + s->mutable_header()->set_value("value-to-check"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_ADD_IF_ABSENT); + break; + case 2: + s->mutable_header()->set_key("x-check-this-header"); + s->mutable_header()->set_value("value-to-check"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS_OR_ADD); + break; + case 3: + headers.addCopy("x-check-this-header", "to be updated"); + s->mutable_header()->set_key("x-check-this-header"); + s->mutable_header()->set_value("value-to-check"); + s->set_append_action(envoy::config::core::v3::HeaderValueOption_HeaderAppendAction:: + HeaderValueOption_HeaderAppendAction_OVERWRITE_IF_EXISTS); + break; + case 4: + scoped_runtime.mergeValues({ + {"envoy.reloadable_features.send_header_raw_value", "false"}, + }); + s->mutable_header()->set_key("x-check-this-header"); + s->mutable_header()->set_value("value-to-check"); + s->mutable_append()->set_value(true); + } + + HeaderMutationRules rules; + rules.mutable_disallow_all()->set_value(true); + rules.mutable_disallow_is_error()->set_value(true); + Checker checker(rules); + + Envoy::Stats::MockCounter rejections; + EXPECT_CALL(rejections, inc()); // Expect 1 rejection + + const auto result = + MutationUtils::applyHeaderMutations(mutation, headers, false, checker, rejections); + + // Assert that the result is an InvalidArgumentError with the specified error message + EXPECT_EQ(result.code(), absl::StatusCode::kInvalidArgument); + EXPECT_EQ(result.message(), "Invalid attempt to modify x-check-this-header"); +} // namespace + } // namespace } // namespace ExternalProcessing } // namespace HttpFilters