diff --git a/docs/root/configuration/http/http_filters/lua_filter.rst b/docs/root/configuration/http/http_filters/lua_filter.rst index 84dc87a8b63f9..1266677372714 100644 --- a/docs/root/configuration/http/http_filters/lua_filter.rst +++ b/docs/root/configuration/http/http_filters/lua_filter.rst @@ -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 `. bodyChunks() diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 8f26c926204ed..1546742e2b845 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -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 ` to configure it. * kill_request: enable a way to configure kill header name in KillRequest proto. * listener: injection of the :ref:`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 ` 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 ` to limit ejection time growth when a node stays unhealthy for extended period of time. By default :ref:`max_ejection_time ` limits ejection time to 5 minutes. Additionally, when the node stays healthy, ejection time decreases. See :ref:`ejection algorithm` for more info. Previously, ejection time could grow without limit and never decreased. diff --git a/source/extensions/filters/http/lua/lua_filter.cc b/source/extensions/filters/http/lua/lua_filter.cc index f8e1e75319161..446635acac8a1 100644 --- a/source/extensions/filters/http/lua/lua_filter.cc +++ b/source/extensions/filters/http/lua/lua_filter.cc @@ -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(*callbacks_.bufferedBody())), true); diff --git a/test/extensions/filters/http/lua/lua_integration_test.cc b/test/extensions/filters/http/lua/lua_integration_test.cc index ca675b77c42a2..342259997c13d 100644 --- a/test/extensions/filters/http/lua/lua_integration_test.cc +++ b/test/extensions/filters/http/lua/lua_integration_test.cc @@ -146,7 +146,7 @@ class LuaIntegrationTest : public testing::TestWithParamencodeHeaders(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) { @@ -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") + 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 =