Skip to content

Commit 7a212bc

Browse files
authored
feat: add knob to customise on{Request,Response}Headers StopIteration behavior (#434)
Add protected ContextBase::allow_on_headers_stop_iteration_ field that can be used by host implementations to control whether or not ContextBase propagates FilterHeaderStatus::StopIteration returned by onRequestHeaders() or onResponseHeaders() without modification. Follow-on envoyproxy/envoy#40213 adds an option in Envoy WasmFilter PluginConfig that sets the value of this field. For details, see [Envoy Wasm / Proxy-Wasm support for FilterHeadersStatus::StopIteration](https://docs.google.com/document/d/1Whd1C0k-H2NHrPOmlAqqauFz6ObSTP017juJIYyciB0/edit?usp=sharing). This PR is one part of implementing [Option B: WasmFilter config knob](https://docs.google.com/document/d/1Whd1C0k-H2NHrPOmlAqqauFz6ObSTP017juJIYyciB0/edit?tab=t.0#bookmark=id.5wxldlapsp54). Note that default behavior of proxy-wasm-cpp-host and ContextBase is unchanged. --------- Signed-off-by: Michael Warres <[email protected]>
1 parent 3095c68 commit 7a212bc

File tree

7 files changed

+147
-4
lines changed

7 files changed

+147
-4
lines changed

include/proxy-wasm/context.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,13 @@ class ContextBase : public RootInterface,
397397
bool destroyed_ = false;
398398
bool stream_failed_ = false; // Set true after failStream is called in case of VM failure.
399399

400+
// If true, convertVmCallResultToFilterHeadersStatus() propagates
401+
// FilterHeadersStatus::StopIteration unmodified to callers. If false, it
402+
// translates FilterHeaderStatus::StopIteration to
403+
// FilterHeadersStatus::StopAllIterationAndWatermark, which is the default
404+
// behavior for v0.2.* of the Proxy-Wasm ABI.
405+
bool allow_on_headers_stop_iteration_ = false;
406+
400407
private:
401408
// helper functions
402409
FilterHeadersStatus convertVmCallResultToFilterHeadersStatus(uint64_t result);

src/context.cc

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -493,10 +493,12 @@ FilterHeadersStatus ContextBase::convertVmCallResultToFilterHeadersStatus(uint64
493493
result > static_cast<uint64_t>(FilterHeadersStatus::StopAllIterationAndWatermark)) {
494494
return FilterHeadersStatus::StopAllIterationAndWatermark;
495495
}
496-
if (result == static_cast<uint64_t>(FilterHeadersStatus::StopIteration)) {
497-
// Always convert StopIteration (pause processing headers, but continue processing body)
498-
// to StopAllIterationAndWatermark (pause all processing), since the former breaks all
499-
// assumptions about HTTP processing.
496+
if (result == static_cast<uint64_t>(FilterHeadersStatus::StopIteration) &&
497+
!allow_on_headers_stop_iteration_) {
498+
// Default behavior for Proxy-Wasm 0.2.* ABI is to translate StopIteration
499+
// (pause processing headers, but continue processing body) to
500+
// StopAllIterationAndWatermark (pause all processing), as described in
501+
// https://github.com/proxy-wasm/proxy-wasm-cpp-host/issues/143.
500502
return FilterHeadersStatus::StopAllIterationAndWatermark;
501503
}
502504
return static_cast<FilterHeadersStatus>(result);

test/BUILD

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,21 @@ cc_test(
132132
],
133133
)
134134

135+
cc_test(
136+
name = "stop_iteration_test",
137+
srcs = ["stop_iteration_test.cc"],
138+
data = [
139+
"//test/test_data:stop_iteration.wasm",
140+
],
141+
linkstatic = 1,
142+
deps = [
143+
":utility_lib",
144+
"//:lib",
145+
"@com_google_googletest//:gtest",
146+
"@com_google_googletest//:gtest_main",
147+
],
148+
)
149+
135150
cc_test(
136151
name = "security_test",
137152
srcs = ["security_test.cc"],

test/stop_iteration_test.cc

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "gtest/gtest.h"
16+
#include "include/proxy-wasm/wasm.h"
17+
#include "test/utility.h"
18+
19+
namespace proxy_wasm {
20+
21+
INSTANTIATE_TEST_SUITE_P(WasmEngines, TestVm, testing::ValuesIn(getWasmEngines()),
22+
[](const testing::TestParamInfo<std::string> &info) {
23+
return info.param;
24+
});
25+
26+
// TestVm is parameterized for each engine and creates a VM on construction.
27+
TEST_P(TestVm, AllowOnHeadersStopIteration) {
28+
// Read the wasm source.
29+
auto source = readTestWasmFile("stop_iteration.wasm");
30+
ASSERT_FALSE(source.empty());
31+
32+
// Create a WasmBase and load the plugin.
33+
auto wasm = std::make_shared<TestWasm>(std::move(vm_));
34+
ASSERT_TRUE(wasm->load(source, /*allow_precompiled=*/false));
35+
ASSERT_TRUE(wasm->initialize());
36+
37+
// Create a plugin.
38+
const auto plugin = std::make_shared<PluginBase>(
39+
/*name=*/"test", /*root_id=*/"", /*vm_id=*/"",
40+
/*engine=*/wasm->wasm_vm()->getEngineName(), /*plugin_config=*/"",
41+
/*fail_open=*/false, /*key=*/"");
42+
43+
// Create root context, call onStart() and onConfigure()
44+
ContextBase *root_context = wasm->start(plugin);
45+
ASSERT_TRUE(root_context != nullptr);
46+
ASSERT_TRUE(wasm->configure(root_context, plugin));
47+
48+
auto wasm_handle = std::make_shared<WasmHandleBase>(wasm);
49+
auto plugin_handle = std::make_shared<PluginHandleBase>(wasm_handle, plugin);
50+
51+
// By default, stream context onRequestHeaders and onResponseHeaders
52+
// translates FilterHeadersStatus::StopIteration to
53+
// FilterHeadersStatus::StopAllIterationAndWatermark.
54+
{
55+
auto stream_context = TestContext(wasm.get(), root_context->id(), plugin_handle);
56+
stream_context.onCreate();
57+
EXPECT_EQ(stream_context.onRequestHeaders(/*headers=*/0, /*end_of_stream=*/false),
58+
FilterHeadersStatus::StopAllIterationAndWatermark);
59+
EXPECT_EQ(stream_context.onResponseHeaders(/*headers=*/0, /*end_of_stream=*/false),
60+
FilterHeadersStatus::StopAllIterationAndWatermark);
61+
stream_context.onDone();
62+
stream_context.onDelete();
63+
}
64+
ASSERT_FALSE(wasm->isFailed());
65+
66+
// Create a stream context that propagates FilterHeadersStatus::StopIteration.
67+
{
68+
auto stream_context = TestContext(wasm.get(), root_context->id(), plugin_handle);
69+
stream_context.set_allow_on_headers_stop_iteration(true);
70+
stream_context.onCreate();
71+
EXPECT_EQ(stream_context.onRequestHeaders(/*headers=*/0, /*end_of_stream=*/false),
72+
FilterHeadersStatus::StopIteration);
73+
EXPECT_EQ(stream_context.onResponseHeaders(/*headers=*/0, /*end_of_stream=*/false),
74+
FilterHeadersStatus::StopIteration);
75+
stream_context.onDone();
76+
stream_context.onDelete();
77+
}
78+
ASSERT_FALSE(wasm->isFailed());
79+
}
80+
81+
} // namespace proxy_wasm

test/test_data/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,8 @@ proxy_wasm_cc_binary(
8989
name = "http_logging.wasm",
9090
srcs = ["http_logging.cc"],
9191
)
92+
93+
proxy_wasm_cc_binary(
94+
name = "stop_iteration.wasm",
95+
srcs = ["stop_iteration.cc"],
96+
)

test/test_data/stop_iteration.cc

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "proxy_wasm_intrinsics.h"
16+
17+
class StopIterationContext : public Context {
18+
public:
19+
explicit StopIterationContext(uint32_t id, RootContext *root) : Context(id, root) {}
20+
21+
FilterHeadersStatus onRequestHeaders(uint32_t headers, bool end_of_stream) override {
22+
return FilterHeadersStatus::StopIteration;
23+
}
24+
25+
FilterHeadersStatus onResponseHeaders(uint32_t headers, bool end_of_stream) override {
26+
return FilterHeadersStatus::StopIteration;
27+
}
28+
};
29+
30+
static RegisterContextFactory register_StaticContext(CONTEXT_FACTORY(StopIterationContext),
31+
ROOT_FACTORY(RootContext));

test/utility.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ class TestContext : public ContextBase {
133133
.count();
134134
}
135135

136+
void set_allow_on_headers_stop_iteration(bool allow) { allow_on_headers_stop_iteration_ = allow; }
137+
136138
private:
137139
std::string log_;
138140
static std::string global_log_;

0 commit comments

Comments
 (0)