Skip to content
6 changes: 5 additions & 1 deletion docs/root/configuration/http/http_filters/lua_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -327,13 +327,17 @@ body()

.. code-block:: lua

local body = handle:body()
local body = handle:body(always_wrap_body)

Returns the stream's body. This call will cause Envoy to suspend execution of the script until
the entire body has been received in a buffer. Note that all buffering must adhere to the
flow-control policies in place. Envoy will not buffer more data than is allowed by the connection
manager.

An optional boolean argument `always_wrap_body` can be used to require Envoy always returns a
`body` object even if the body is empty. Therefore we can modify the body regardless of whether the
original body exists or not.

Returns a :ref:`buffer object <config_http_filters_lua_buffer_wrapper>`.

bodyChunks()
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Minor Behavior Changes
* jwt_authn filter: added support of Jwt time constraint verification with a clock skew (default to 60 seconds) and added a filter config field :ref:`clock_skew_seconds <envoy_v3_api_field_extensions.filters.http.jwt_authn.v3.JwtProvider.clock_skew_seconds>` to configure it.
* kill_request: enable a way to configure kill header name in KillRequest proto.
* listener: injection of the :ref:`TLS inspector <config_listener_filters_tls_inspector>` has been disabled by default. This feature is controlled by the runtime guard `envoy.reloadable_features.disable_tls_inspector_injection`.
* lua: add `always_wrap_body` argument to `body()` API to always return a :ref:`buffer object <config_http_filters_lua_buffer_wrapper>` even if the body is empty.
* memory: enable new tcmalloc with restartable sequences for aarch64 builds.
* mongo proxy metrics: swapped network connection remote and local closed counters previously set reversed (`cx_destroy_local_with_active_rq` and `cx_destroy_remote_with_active_rq`).
* outlier detection: added :ref:`max_ejection_time <envoy_v3_api_field_config.cluster.v3.OutlierDetection.max_ejection_time>` to limit ejection time growth when a node stays unhealthy for extended period of time. By default :ref:`max_ejection_time <envoy_v3_api_field_config.cluster.v3.OutlierDetection.max_ejection_time>` limits ejection time to 5 minutes. Additionally, when the node stays healthy, ejection time decreases. See :ref:`ejection algorithm<arch_overview_outlier_detection_algorithm>` for more info. Previously, ejection time could grow without limit and never decreased.
Expand Down
21 changes: 18 additions & 3 deletions source/extensions/filters/http/lua/lua_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -418,16 +418,31 @@ int StreamHandleWrapper::luaHeaders(lua_State* state) {
int StreamHandleWrapper::luaBody(lua_State* state) {
ASSERT(state_ == State::Running);

bool always_wrap_body = false;

if (lua_gettop(state) >= 2) {
luaL_checktype(state, 2, LUA_TBOOLEAN);
always_wrap_body = lua_toboolean(state, 2);
}

if (end_stream_) {
if (!buffered_body_ && saw_body_) {
return luaL_error(state, "cannot call body() after body has been streamed");
} else if (callbacks_.bufferedBody() == nullptr) {
ENVOY_LOG(debug, "end stream. no body");
return 0;
} else {
if (body_wrapper_.get() != nullptr) {
body_wrapper_.pushStack();
} else {
if (callbacks_.bufferedBody() == nullptr) {
ENVOY_LOG(debug, "end stream. no body");

if (!always_wrap_body) {
return 0;
}

Buffer::OwnedImpl body(EMPTY_STRING);
callbacks_.addData(body);
}

body_wrapper_.reset(Filters::Common::Lua::BufferWrapper::create(
state, const_cast<Buffer::Instance&>(*callbacks_.bufferedBody())),
true);
Expand Down
81 changes: 70 additions & 11 deletions test/extensions/filters/http/lua/lua_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class LuaIntegrationTest : public testing::TestWithParam<Network::Address::IpVer
registerTestServerPorts({"http"});
}

void testRewriteResponse(const std::string& code) {
void expectResponseBodyRewrite(const std::string& code, bool empty_body, bool enable_wrap_body) {
initializeFilter(code);
codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http")));
Http::TestRequestHeaderMapImpl request_headers{{":method", "POST"},
Expand All @@ -164,22 +164,40 @@ class LuaIntegrationTest : public testing::TestWithParam<Network::Address::IpVer
waitForNextUpstreamRequest();

Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}, {"foo", "bar"}};
upstream_request_->encodeHeaders(response_headers, false);
Buffer::OwnedImpl response_data1("good");
upstream_request_->encodeData(response_data1, false);
Buffer::OwnedImpl response_data2("bye");
upstream_request_->encodeData(response_data2, true);

if (empty_body) {
upstream_request_->encodeHeaders(response_headers, true);
} else {
upstream_request_->encodeHeaders(response_headers, false);
Buffer::OwnedImpl response_data1("good");
upstream_request_->encodeData(response_data1, false);
Buffer::OwnedImpl response_data2("bye");
upstream_request_->encodeData(response_data2, true);
}

response->waitForEndStream();

EXPECT_EQ("2", response->headers()
.get(Http::LowerCaseString("content-length"))[0]
->value()
.getStringView());
EXPECT_EQ("ok", response->body());
if (enable_wrap_body) {
EXPECT_EQ("2", response->headers()
.get(Http::LowerCaseString("content-length"))[0]
->value()
.getStringView());
EXPECT_EQ("ok", response->body());
} else {
EXPECT_EQ("", response->body());
}

cleanup();
}

void testRewriteResponse(const std::string& code) {
expectResponseBodyRewrite(code, false, true);
}

void testRewriteResponseWithoutUpstreamBody(const std::string& code, bool enable_wrap_body) {
expectResponseBodyRewrite(code, true, enable_wrap_body);
}

void cleanup() {
codec_client_->close();
if (fake_lua_connection_ != nullptr) {
Expand Down Expand Up @@ -974,6 +992,47 @@ name: lua
testRewriteResponse(FILTER_AND_CODE);
}

// Rewrite response buffer, without original upstream response body
// and always wrap body.
TEST_P(LuaIntegrationTest, RewriteResponseBufferWithoutUpstreamBody) {
const std::string FILTER_AND_CODE =
R"EOF(
name: lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_response(response_handle)
local content_length = response_handle:body(true):setBytes("ok")
response_handle:logTrace(content_length)

response_handle:headers():replace("content-length", content_length)
end
)EOF";

testRewriteResponseWithoutUpstreamBody(FILTER_AND_CODE, true);
}

// Rewrite response buffer, without original upstream response body
// and don't always wrap body.
TEST_P(LuaIntegrationTest, RewriteResponseBufferWithoutUpstreamBodyAndDisableWrapBody) {
const std::string FILTER_AND_CODE =
R"EOF(
name: lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inline_code: |
function envoy_on_response(response_handle)
if response_handle:body(false) then
local content_length = response_handle:body():setBytes("ok")
Copy link
Member

Choose a reason for hiding this comment

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

I'm confused. Shouldn't this end up returning nil and the script should crash? Shouldn't we be verifying nil here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The response_handle:body(false) always returns nil so this branch is skipped. It is just a mirror of the previous test but with the false given.

Copy link
Member

Choose a reason for hiding this comment

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

Ah sorry, I missed that. OK LGTM modulo the typo.

response_handle:logTrace(content_length)
response_handle:headers():replace("content-length", content_length)
end
end
)EOF";

testRewriteResponseWithoutUpstreamBody(FILTER_AND_CODE, false);
}

// Rewrite chunked response body.
TEST_P(LuaIntegrationTest, RewriteChunkedBody) {
const std::string FILTER_AND_CODE =
Expand Down