diff --git a/library/common/extensions/filters/http/platform_bridge/BUILD b/library/common/extensions/filters/http/platform_bridge/BUILD index 93892c6802..f0fac266c3 100644 --- a/library/common/extensions/filters/http/platform_bridge/BUILD +++ b/library/common/extensions/filters/http/platform_bridge/BUILD @@ -10,7 +10,7 @@ api_proto_package() envoy_cc_library( name = "platform_bridge_filter_lib", srcs = [ - "c_types.cc", + "c_type_definitions.h", "filter.cc", ], hdrs = [ diff --git a/library/common/extensions/filters/http/platform_bridge/c_types.cc b/library/common/extensions/filters/http/platform_bridge/c_type_definitions.h similarity index 61% rename from library/common/extensions/filters/http/platform_bridge/c_types.cc rename to library/common/extensions/filters/http/platform_bridge/c_type_definitions.h index adbb213d93..25adb2f30e 100644 --- a/library/common/extensions/filters/http/platform_bridge/c_types.cc +++ b/library/common/extensions/filters/http/platform_bridge/c_type_definitions.h @@ -1,8 +1,8 @@ // NOLINT(namespace-envoy) -#include "library/common/extensions/filters/http/platform_bridge/c_types.h" - #include "envoy/http/filter.h" +#include "library/common/extensions/filters/http/platform_bridge/c_types.h" + const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusContinue = static_cast(Envoy::Http::FilterHeadersStatus::Continue); const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusStopIteration = @@ -13,6 +13,19 @@ const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusContinueAndEndStrea const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusStopAllIterationAndBuffer = static_cast( Envoy::Http::FilterHeadersStatus::StopAllIterationAndBuffer); +// ResumeIteration is not a status supported by Envoy itself, and only has relevance in Envoy +// Mobile's implementation of platform filters. +// +// Regarding enum values, the C++11 standard (7.2/2) states: +// If the first enumerator has no initializer, the value of the corresponding constant is zero. +// An enumerator-definition without an initializer gives the enumerator the value obtained by +// increasing the value of the previous enumerator by one. +// +// Creating a new return status like this is brittle, but at least somewhat more resilient to +// a new status being added in Envoy, since it won't overlap as long as the new status is added +// rather than prepended. +const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusResumeIteration = + kEnvoyFilterHeadersStatusContinue - 1; const envoy_filter_data_status_t kEnvoyFilterDataStatusContinue = static_cast(Envoy::Http::FilterDataStatus::Continue); @@ -20,8 +33,14 @@ const envoy_filter_data_status_t kEnvoyFilterDataStatusStopIterationAndBuffer = static_cast(Envoy::Http::FilterDataStatus::StopIterationAndBuffer); const envoy_filter_data_status_t kEnvoyFilterDataStatusStopIterationNoBuffer = static_cast(Envoy::Http::FilterDataStatus::StopIterationNoBuffer); +// See comment above. +const envoy_filter_data_status_t kEnvoyFilterDataStatusResumeIteration = + kEnvoyFilterDataStatusContinue - 1; const envoy_filter_trailers_status_t kEnvoyFilterTrailersStatusContinue = static_cast(Envoy::Http::FilterTrailersStatus::Continue); const envoy_filter_trailers_status_t kEnvoyFilterTrailersStatusStopIteration = static_cast(Envoy::Http::FilterTrailersStatus::StopIteration); +// See comment above. +const envoy_filter_trailers_status_t kEnvoyFilterTrailersStatusResumeIteration = + kEnvoyFilterTrailersStatusContinue - 1; diff --git a/library/common/extensions/filters/http/platform_bridge/c_types.h b/library/common/extensions/filters/http/platform_bridge/c_types.h index ca5e03d4e4..dbba792d8d 100644 --- a/library/common/extensions/filters/http/platform_bridge/c_types.h +++ b/library/common/extensions/filters/http/platform_bridge/c_types.h @@ -4,6 +4,16 @@ // NOLINT(namespace-envoy) +/** + * Convenience constant indicating no changes to data. + */ +extern const envoy_data envoy_unaltered_data; + +/** + * Convenience constant indicating no changes to headers. + */ +extern const envoy_headers envoy_unaltered_headers; + /** * Return codes for on-headers filter invocations. @see envoy/http/filter.h */ @@ -12,6 +22,9 @@ extern const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusContinue; extern const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusStopIteration; extern const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusContinueAndEndStream; extern const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusStopAllIterationAndBuffer; +// Note this return status is unique to platform filters and used only to resume iteration after +// it has been previously stopped. +extern const envoy_filter_headers_status_t kEnvoyFilterHeadersStatusResumeIteration; /** * Compound return type for on-headers filter invocations. @@ -28,6 +41,9 @@ typedef int envoy_filter_data_status_t; extern const envoy_filter_data_status_t kEnvoyFilterDataStatusContinue; extern const envoy_filter_data_status_t kEnvoyFilterDataStatusStopIterationAndBuffer; extern const envoy_filter_data_status_t kEnvoyFilterDataStatusStopIterationNoBuffer; +// Note this return status is unique to platform filters and used only to resume iteration after +// it has been previously stopped. +extern const envoy_filter_data_status_t kEnvoyFilterDataStatusResumeIteration; /** * Compound return type for on-data filter invocations. @@ -35,6 +51,7 @@ extern const envoy_filter_data_status_t kEnvoyFilterDataStatusStopIterationNoBuf typedef struct { envoy_filter_data_status_t status; envoy_data data; + envoy_headers* pending_headers; } envoy_filter_data_status; /** @@ -43,6 +60,9 @@ typedef struct { typedef int envoy_filter_trailers_status_t; extern const envoy_filter_trailers_status_t kEnvoyFilterTrailersStatusContinue; extern const envoy_filter_trailers_status_t kEnvoyFilterTrailersStatusStopIteration; +// Note this return status is unique to platform filters and used only to resume iteration after +// it has been previously stopped. +extern const envoy_filter_trailers_status_t kEnvoyFilterTrailersStatusResumeIteration; /** * Compound return type for on-trailers filter invocations. @@ -50,6 +70,8 @@ extern const envoy_filter_trailers_status_t kEnvoyFilterTrailersStatusStopIterat typedef struct { envoy_filter_trailers_status_t status; envoy_headers trailers; + envoy_headers* pending_headers; + envoy_data* pending_data; } envoy_filter_trailers_status; #ifdef __cplusplus diff --git a/library/common/extensions/filters/http/platform_bridge/filter.cc b/library/common/extensions/filters/http/platform_bridge/filter.cc index f4f4a95ec2..dc81cbe27c 100644 --- a/library/common/extensions/filters/http/platform_bridge/filter.cc +++ b/library/common/extensions/filters/http/platform_bridge/filter.cc @@ -8,6 +8,7 @@ #include "library/common/api/external.h" #include "library/common/buffer/bridge_fragment.h" #include "library/common/buffer/utility.h" +#include "library/common/extensions/filters/http/platform_bridge/c_type_definitions.h" #include "library/common/http/header_utility.h" namespace Envoy { @@ -56,6 +57,16 @@ void PlatformBridgeFilter::onDestroy() { platform_filter_.instance_context = nullptr; } +void PlatformBridgeFilter::replaceHeaders(Http::HeaderMap& headers, envoy_headers c_headers) { + headers.clear(); + for (envoy_header_size_t i = 0; i < c_headers.length; i++) { + headers.addCopy(Http::LowerCaseString(Http::Utility::convertToString(c_headers.headers[i].key)), + Http::Utility::convertToString(c_headers.headers[i].value)); + } + // The C envoy_headers struct can be released now because the headers have been copied. + release_envoy_headers(c_headers); +} + Http::FilterHeadersStatus PlatformBridgeFilter::onHeaders(Http::HeaderMap& headers, bool end_stream, envoy_filter_on_headers_f on_headers) { // Allow nullptr to act as no-op. @@ -66,23 +77,26 @@ Http::FilterHeadersStatus PlatformBridgeFilter::onHeaders(Http::HeaderMap& heade envoy_headers in_headers = Http::Utility::toBridgeHeaders(headers); envoy_filter_headers_status result = on_headers(in_headers, end_stream, platform_filter_.instance_context); - Http::FilterHeadersStatus status = static_cast(result.status); - // TODO(goaway): Current platform implementations expose immutable headers, thus any modification - // necessitates a full copy. Add 'modified' bit to determine when we can elide the copy. See also - // https://github.com/lyft/envoy-mobile/issues/949 for potential future optimization. - headers.clear(); - for (envoy_header_size_t i = 0; i < result.headers.length; i++) { - headers.addCopy( - Http::LowerCaseString(Http::Utility::convertToString(result.headers.headers[i].key)), - Http::Utility::convertToString(result.headers.headers[i].value)); + + switch (result.status) { + case kEnvoyFilterHeadersStatusContinue: + PlatformBridgeFilter::replaceHeaders(headers, result.headers); + return Http::FilterHeadersStatus::Continue; + + case kEnvoyFilterHeadersStatusStopIteration: + iteration_state_ = IterationState::Stopped; + return Http::FilterHeadersStatus::StopIteration; + + default: + PANIC("invalid filter state: unsupported status for platform filters"); } - // The C envoy_headers struct can be released now because the headers have been copied. - release_envoy_headers(result.headers); - return status; + + NOT_REACHED_GCOVR_EXCL_LINE; } Http::FilterDataStatus PlatformBridgeFilter::onData(Buffer::Instance& data, bool end_stream, Buffer::Instance* internal_buffer, + Http::HeaderMap** pending_headers, envoy_filter_on_data_f on_data) { // Allow nullptr to act as no-op. if (on_data == nullptr) { @@ -90,9 +104,10 @@ Http::FilterDataStatus PlatformBridgeFilter::onData(Buffer::Instance& data, bool } envoy_data in_data; + bool already_buffering = iteration_state_ == IterationState::Stopped && internal_buffer && + internal_buffer->length() > 0; - if (iteration_state_ == IterationState::Stopped && internal_buffer && - internal_buffer->length() > 0) { + if (already_buffering) { // Pre-emptively buffer data to present aggregate to platform. internal_buffer->move(data); in_data = Buffer::Utility::copyToBridgeData(*internal_buffer); @@ -101,27 +116,25 @@ Http::FilterDataStatus PlatformBridgeFilter::onData(Buffer::Instance& data, bool } envoy_filter_data_status result = on_data(in_data, end_stream, platform_filter_.instance_context); - Http::FilterDataStatus status = static_cast(result.status); - switch (status) { - case Http::FilterDataStatus::Continue: - if (iteration_state_ == IterationState::Stopped) { - // When platform filter iteration is Stopped, Resume must be used to start iterating again. - // TODO(goaway): decide on the means to surface/handle errors here. Options include: - // - crashing - // - creating an error response for this stream - // - letting Envoy handle any invalid resulting state via its own guards - } - break; - case Http::FilterDataStatus::StopIterationAndBuffer: - if (iteration_state_ == IterationState::Stopped) { - // Data will already have been buffered (above). - status = Http::FilterDataStatus::StopIterationNoBuffer; - } else { - // Data will be buffered on return. - iteration_state_ = IterationState::Stopped; + + switch (result.status) { + case kEnvoyFilterDataStatusContinue: + RELEASE_ASSERT(iteration_state_ != IterationState::Stopped, + "invalid filter state: filter iteration must be resumed with ResumeIteration"); + data.drain(data.length()); + data.addBufferFragment(*Buffer::BridgeFragment::createBridgeFragment(result.data)); + return Http::FilterDataStatus::Continue; + + case kEnvoyFilterDataStatusStopIterationAndBuffer: + if (already_buffering) { + // Data will already have been added to the internal buffer (above). + return Http::FilterDataStatus::StopIterationNoBuffer; } - break; - case Http::FilterDataStatus::StopIterationNoBuffer: + // Data will be buffered on return. + iteration_state_ = IterationState::Stopped; + return Http::FilterDataStatus::StopIterationAndBuffer; + + case kEnvoyFilterDataStatusStopIterationNoBuffer: // In this context all previously buffered data can/should be dropped. If no data has been // buffered, this is a no-op. If data was previously buffered, the most likely case is // that a filter has decided to handle generating a response itself and no longer needs it. @@ -133,24 +146,43 @@ Http::FilterDataStatus PlatformBridgeFilter::onData(Buffer::Instance& data, bool internal_buffer->drain(internal_buffer->length()); } iteration_state_ = IterationState::Stopped; - break; - default: - PANIC("unsupported status for platform filters"); - } + return Http::FilterDataStatus::StopIterationNoBuffer; - // TODO(goaway): Current platform implementations expose immutable data, thus any modification - // necessitates a full copy. Add 'modified' bit to determine when we can elide the copy. See also - // https://github.com/lyft/envoy-mobile/issues/949 for potential future optimization. - if (iteration_state_ == IterationState::Ongoing) { - data.drain(data.length()); - data.addBufferFragment(*Buffer::BridgeFragment::createBridgeFragment(result.data)); + // Resume previously-stopped iteration, possibly forwarding headers if iteration was stopped + // during an on*Headers invocation. + case kEnvoyFilterDataStatusResumeIteration: + RELEASE_ASSERT(iteration_state_ == IterationState::Stopped, + "invalid filter state: ResumeIteration may only be used when filter iteration " + "is stopped"); + // Update pending henders before resuming iteration, if needed. + if (result.pending_headers) { + PlatformBridgeFilter::replaceHeaders(**pending_headers, *result.pending_headers); + *pending_headers = nullptr; + free(result.pending_headers); + } + // We've already moved data into the internal buffer and presented it to the platform. Replace + // the internal buffer with any modifications returned by the platform filter prior to + // resumption. + if (internal_buffer) { + internal_buffer->drain(internal_buffer->length()); + internal_buffer->addBufferFragment( + *Buffer::BridgeFragment::createBridgeFragment(result.data)); + } else { + data.drain(data.length()); + data.addBufferFragment(*Buffer::BridgeFragment::createBridgeFragment(result.data)); + } + return Http::FilterDataStatus::Continue; + + default: + PANIC("invalid filter state: unsupported status for platform filters"); } - return status; + NOT_REACHED_GCOVR_EXCL_LINE; } Http::FilterTrailersStatus -PlatformBridgeFilter::onTrailers(Http::HeaderMap& trailers, +PlatformBridgeFilter::onTrailers(Http::HeaderMap& trailers, Buffer::Instance* internal_buffer, + Http::HeaderMap** pending_headers, envoy_filter_on_trailers_f on_trailers) { // Allow nullptr to act as no-op. if (on_trailers == nullptr) { @@ -159,25 +191,68 @@ PlatformBridgeFilter::onTrailers(Http::HeaderMap& trailers, envoy_headers in_trailers = Http::Utility::toBridgeHeaders(trailers); envoy_filter_trailers_status result = on_trailers(in_trailers, platform_filter_.instance_context); - Http::FilterTrailersStatus status = static_cast(result.status); - // TODO(goaway): Current platform implementations expose immutable trailers, thus any modification - // necessitates a full copy. Add 'modified' bit to determine when we can elide the copy. See also - // https://github.com/lyft/envoy-mobile/issues/949 for potential future optimization. - trailers.clear(); - for (envoy_header_size_t i = 0; i < result.trailers.length; i++) { - trailers.addCopy( - Http::LowerCaseString(Http::Utility::convertToString(result.trailers.headers[i].key)), - Http::Utility::convertToString(result.trailers.headers[i].value)); - } - // The C envoy_trailers struct can be released now because the trailers have been copied. - release_envoy_headers(result.trailers); - return status; + + switch (result.status) { + case kEnvoyFilterTrailersStatusContinue: + RELEASE_ASSERT(iteration_state_ != IterationState::Stopped, + "invalid filter state: ResumeIteration may only be used when filter iteration " + "is stopped"); + PlatformBridgeFilter::replaceHeaders(trailers, result.trailers); + return Http::FilterTrailersStatus::Continue; + + case kEnvoyFilterTrailersStatusStopIteration: + iteration_state_ = IterationState::Stopped; + return Http::FilterTrailersStatus::StopIteration; + + // Resume previously-stopped iteration, possibly forwarding headers and data if iteration was + // stopped during an on*Headers or on*Data invocation. + case kEnvoyFilterTrailersStatusResumeIteration: + RELEASE_ASSERT(iteration_state_ == IterationState::Stopped, + "invalid filter state: ResumeIteration may only be used when filter iteration " + "is stopped"); + // Update pending henders before resuming iteration, if needed. + if (result.pending_headers) { + PlatformBridgeFilter::replaceHeaders(**pending_headers, *result.pending_headers); + *pending_headers = nullptr; + free(result.pending_headers); + } + // We've already moved data into the internal buffer and presented it to the platform. Replace + // the internal buffer with any modifications returned by the platform filter prior to + // resumption. + if (result.pending_data) { + internal_buffer->drain(internal_buffer->length()); + internal_buffer->addBufferFragment( + *Buffer::BridgeFragment::createBridgeFragment(*result.pending_data)); + free(result.pending_data); + } + PlatformBridgeFilter::replaceHeaders(trailers, result.trailers); + return Http::FilterTrailersStatus::Continue; + + default: + PANIC("invalid filter state: unsupported status for platform filters"); + } + + NOT_REACHED_GCOVR_EXCL_LINE; } Http::FilterHeadersStatus PlatformBridgeFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) { // Delegate to shared implementation for request and response path. - return onHeaders(headers, end_stream, platform_filter_.on_request_headers); + auto status = onHeaders(headers, end_stream, platform_filter_.on_request_headers); + if (status == Http::FilterHeadersStatus::StopIteration) { + pending_request_headers_ = &headers; + } + return status; +} + +Http::FilterHeadersStatus PlatformBridgeFilter::encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) { + // Delegate to shared implementation for request and response path. + auto status = onHeaders(headers, end_stream, platform_filter_.on_response_headers); + if (status == Http::FilterHeadersStatus::StopIteration) { + pending_response_headers_ = &headers; + } + return status; } Http::FilterDataStatus PlatformBridgeFilter::decodeData(Buffer::Instance& data, bool end_stream) { @@ -189,36 +264,56 @@ Http::FilterDataStatus PlatformBridgeFilter::decodeData(Buffer::Instance& data, }); } - return onData(data, end_stream, internal_buffer, platform_filter_.on_request_data); + return onData(data, end_stream, internal_buffer, &pending_request_headers_, + platform_filter_.on_request_data); } -Http::FilterTrailersStatus PlatformBridgeFilter::decodeTrailers(Http::RequestTrailerMap& trailers) { +Http::FilterDataStatus PlatformBridgeFilter::encodeData(Buffer::Instance& data, bool end_stream) { // Delegate to shared implementation for request and response path. - return onTrailers(trailers, platform_filter_.on_request_trailers); -} + Buffer::Instance* internal_buffer = nullptr; + if (encoder_callbacks_->encodingBuffer()) { + encoder_callbacks_->modifyEncodingBuffer([&internal_buffer](Buffer::Instance& mutable_buffer) { + internal_buffer = &mutable_buffer; + }); + } -Http::FilterHeadersStatus PlatformBridgeFilter::encodeHeaders(Http::ResponseHeaderMap& headers, - bool end_stream) { - // Delegate to shared implementation for request and response path. - return onHeaders(headers, end_stream, platform_filter_.on_response_headers); + return onData(data, end_stream, internal_buffer, &pending_response_headers_, + platform_filter_.on_response_data); } -Http::FilterDataStatus PlatformBridgeFilter::encodeData(Buffer::Instance& data, bool end_stream) { +Http::FilterTrailersStatus PlatformBridgeFilter::decodeTrailers(Http::RequestTrailerMap& trailers) { // Delegate to shared implementation for request and response path. Buffer::Instance* internal_buffer = nullptr; - if (encoder_callbacks_->encodingBuffer()) { - encoder_callbacks_->modifyEncodingBuffer([&internal_buffer](Buffer::Instance& mutable_buffer) { + if (decoder_callbacks_->decodingBuffer()) { + decoder_callbacks_->modifyDecodingBuffer([&internal_buffer](Buffer::Instance& mutable_buffer) { internal_buffer = &mutable_buffer; }); } - return onData(data, end_stream, internal_buffer, platform_filter_.on_response_data); + auto status = onTrailers(trailers, internal_buffer, &pending_request_headers_, + platform_filter_.on_request_trailers); + if (status == Http::FilterTrailersStatus::StopIteration) { + pending_request_trailers_ = &trailers; + } + return status; } Http::FilterTrailersStatus PlatformBridgeFilter::encodeTrailers(Http::ResponseTrailerMap& trailers) { // Delegate to shared implementation for request and response path. - return onTrailers(trailers, platform_filter_.on_response_trailers); + Buffer::Instance* internal_buffer = nullptr; + if (encoder_callbacks_->encodingBuffer()) { + encoder_callbacks_->modifyEncodingBuffer([&internal_buffer](Buffer::Instance& mutable_buffer) { + internal_buffer = &mutable_buffer; + }); + } + + auto status = onTrailers(trailers, internal_buffer, &pending_response_headers_, + platform_filter_.on_response_trailers); + if (status == Http::FilterTrailersStatus::StopIteration) { + pending_response_trailers_ = &trailers; + } + return status; } } // namespace PlatformBridge diff --git a/library/common/extensions/filters/http/platform_bridge/filter.h b/library/common/extensions/filters/http/platform_bridge/filter.h index f19f3455cd..b2acf80aa0 100644 --- a/library/common/extensions/filters/http/platform_bridge/filter.h +++ b/library/common/extensions/filters/http/platform_bridge/filter.h @@ -33,6 +33,17 @@ enum class IterationState { Ongoing, Stopped }; /** * Harness to bridge Envoy filter invocations up to the platform layer. + * + * This filter enables filter implementations to be written in high-level platform-specific + * languages and run within the Envoy filter chain. To mirror platform API conventions, the + * semantic structure of platform filters differs slightly from Envoy filters. Platform + * filter invocations (on-headers, on-data, etc.) receive *immutable* entities as parameters + * and are expected to return compound results that include both the filter status, as well + * as any desired modifications to the HTTP entity. Additionally, when platform filters + * stop iteration, they _must_ use a new ResumeIteration status to resume iteration + * at a later point. The Continue status is only valid if iteration is already ongoing. + * + * For more information on implementing platform filters, see the docs. */ class PlatformBridgeFilter final : public Http::PassThroughFilter, Logger::Loggable { @@ -55,15 +66,23 @@ class PlatformBridgeFilter final : public Http::PassThroughFilter, Http::FilterTrailersStatus encodeTrailers(Http::ResponseTrailerMap& trailers) override; private: + static void replaceHeaders(Http::HeaderMap& headers, envoy_headers c_headers); Http::FilterHeadersStatus onHeaders(Http::HeaderMap& headers, bool end_stream, envoy_filter_on_headers_f on_headers); Http::FilterDataStatus onData(Buffer::Instance& data, bool end_stream, - Buffer::Instance* internal_buffer, envoy_filter_on_data_f on_data); + Buffer::Instance* internal_buffer, + Http::HeaderMap** pending_headers, envoy_filter_on_data_f on_data); Http::FilterTrailersStatus onTrailers(Http::HeaderMap& trailers, + Buffer::Instance* internal_buffer, + Http::HeaderMap** pending_headers, envoy_filter_on_trailers_f on_trailers); const std::string filter_name_; IterationState iteration_state_; envoy_http_filter platform_filter_; + Http::HeaderMap* pending_request_headers_{}; + Http::HeaderMap* pending_response_headers_{}; + Http::HeaderMap* pending_request_trailers_{}; + Http::HeaderMap* pending_response_trailers_{}; }; } // namespace PlatformBridge diff --git a/library/common/types/c_types.cc b/library/common/types/c_types.cc index 254dcafcd9..a3694a07f1 100644 --- a/library/common/types/c_types.cc +++ b/library/common/types/c_types.cc @@ -58,3 +58,5 @@ envoy_data copy_envoy_data(size_t length, const uint8_t* src_bytes) { } const envoy_data envoy_nodata = {0, NULL, envoy_noop_release, NULL}; + +const envoy_headers envoy_noheaders = {0, NULL}; diff --git a/library/common/types/c_types.h b/library/common/types/c_types.h index da263b4144..4299fce61d 100644 --- a/library/common/types/c_types.h +++ b/library/common/types/c_types.h @@ -149,7 +149,10 @@ envoy_data copy_envoy_data(size_t length, const uint8_t* src_bytes); // For example when sending a headers-only request. extern const envoy_data envoy_nodata; -/** +// Convenience constant to pass to function calls with no headers. +extern const envoy_headers envoy_noheaders; + +/* * Error struct. */ typedef struct { diff --git a/library/kotlin/src/io/envoyproxy/envoymobile/filters/Filter.kt b/library/kotlin/src/io/envoyproxy/envoymobile/filters/Filter.kt index c50acf9567..9d96b432e3 100644 --- a/library/kotlin/src/io/envoyproxy/envoymobile/filters/Filter.kt +++ b/library/kotlin/src/io/envoyproxy/envoymobile/filters/Filter.kt @@ -24,7 +24,7 @@ internal class FilterFactory( internal class EnvoyHTTPFilterAdapter( private val filter: Filter ) : EnvoyHTTPFilter { - override fun onRequestHeaders(headers: Map>, endStream: Boolean): Array { + override fun onRequestHeaders(headers: Map>, endStream: Boolean): Array { (filter as? RequestFilter)?.let { requestFilter -> val result = requestFilter.onRequestHeaders(RequestHeaders(headers), endStream) return when (result) { @@ -35,7 +35,7 @@ internal class EnvoyHTTPFilterAdapter( return arrayOf(0, headers) } - override fun onResponseHeaders(headers: Map>, endStream: Boolean): Array { + override fun onResponseHeaders(headers: Map>, endStream: Boolean): Array { (filter as? ResponseFilter)?.let { responseFilter -> val result = responseFilter.onResponseHeaders(ResponseHeaders(headers), endStream) return when (result) { @@ -46,51 +46,51 @@ internal class EnvoyHTTPFilterAdapter( return arrayOf(0, headers) } - override fun onRequestData(data: ByteBuffer, endStream: Boolean): Array { + override fun onRequestData(data: ByteBuffer, endStream: Boolean): Array { (filter as? RequestFilter)?.let { requestFilter -> val result = requestFilter.onRequestData(data, endStream) return when (result) { is FilterDataStatus.Continue<*> -> arrayOf(result.status, result.data) is FilterDataStatus.StopIterationAndBuffer<*> -> arrayOf(result.status, data) is FilterDataStatus.StopIterationNoBuffer<*> -> arrayOf(result.status, data) - is FilterDataStatus.ResumeIteration<*> -> arrayOf(result.status, result.data) + is FilterDataStatus.ResumeIteration<*> -> arrayOf(result.status, result.headers?.headers, result.data) } } return arrayOf(0, data) } - override fun onResponseData(data: ByteBuffer, endStream: Boolean): Array { + override fun onResponseData(data: ByteBuffer, endStream: Boolean): Array { (filter as? ResponseFilter)?.let { responseFilter -> val result = responseFilter.onResponseData(data, endStream) return when (result) { is FilterDataStatus.Continue<*> -> arrayOf(result.status, result.data) is FilterDataStatus.StopIterationAndBuffer<*> -> arrayOf(result.status, data) is FilterDataStatus.StopIterationNoBuffer<*> -> arrayOf(result.status, data) - is FilterDataStatus.ResumeIteration<*> -> arrayOf(result.status, result.data) + is FilterDataStatus.ResumeIteration<*> -> arrayOf(result.status, result.headers?.headers, result.data) } } return arrayOf(0, data) } - override fun onRequestTrailers(trailers: Map>): Array { + override fun onRequestTrailers(trailers: Map>): Array { (filter as? RequestFilter)?.let { requestFilter -> val result = requestFilter.onRequestTrailers(RequestTrailers(trailers)) return when (result) { is FilterTrailersStatus.Continue<*, *> -> arrayOf(result.status, result.trailers.headers) is FilterTrailersStatus.StopIteration<*, *> -> arrayOf(result.status, trailers) - is FilterTrailersStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.trailers!!.headers) + is FilterTrailersStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.headers?.headers, result.data, result.trailers.headers) } } return arrayOf(0, trailers) } - override fun onResponseTrailers(trailers: Map>): Array { + override fun onResponseTrailers(trailers: Map>): Array { (filter as? ResponseFilter)?.let { responseFilter -> val result = responseFilter.onResponseTrailers(ResponseTrailers(trailers)) return when (result) { is FilterTrailersStatus.Continue<*, *> -> arrayOf(result.status, result.trailers.headers) is FilterTrailersStatus.StopIteration<*, *> -> arrayOf(result.status, trailers) - is FilterTrailersStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.trailers!!.headers) + is FilterTrailersStatus.ResumeIteration<*, *> -> arrayOf(result.status, result.headers?.headers, result.data, result.trailers.headers) } } return arrayOf(0, trailers) diff --git a/library/kotlin/src/io/envoyproxy/envoymobile/filters/FilterTrailersStatus.kt b/library/kotlin/src/io/envoyproxy/envoymobile/filters/FilterTrailersStatus.kt index 1a68a5a47c..e4a1fe93d7 100644 --- a/library/kotlin/src/io/envoyproxy/envoymobile/filters/FilterTrailersStatus.kt +++ b/library/kotlin/src/io/envoyproxy/envoymobile/filters/FilterTrailersStatus.kt @@ -40,6 +40,6 @@ sealed class FilterTrailersStatus( class ResumeIteration( val headers: T?, val data: ByteBuffer?, - val trailers: U? + val trailers: U ) : FilterTrailersStatus(-1) } diff --git a/library/objective-c/EnvoyBridgeUtility.h b/library/objective-c/EnvoyBridgeUtility.h index cc9a3608f1..8702300f44 100644 --- a/library/objective-c/EnvoyBridgeUtility.h +++ b/library/objective-c/EnvoyBridgeUtility.h @@ -3,12 +3,26 @@ #import "library/common/types/c_types.h" static inline envoy_data toNativeData(NSData *data) { + if (data == nil) { + return envoy_nodata; + } + uint8_t *native_bytes = (uint8_t *)safe_malloc(sizeof(uint8_t) * data.length); memcpy(native_bytes, data.bytes, data.length); envoy_data ret = {data.length, native_bytes, free, native_bytes}; return ret; } +static inline envoy_data *toNativeDataPtr(NSData *data) { + if (data == nil) { + return NULL; + } + + envoy_data *ret = (envoy_data *)safe_malloc(sizeof(envoy_data)); + *ret = toNativeData(data); + return ret; +} + static inline envoy_data toManagedNativeString(NSString *s) { size_t length = s.length; uint8_t *native_string = (uint8_t *)safe_malloc(sizeof(uint8_t) * length); @@ -18,6 +32,10 @@ static inline envoy_data toManagedNativeString(NSString *s) { } static inline envoy_headers toNativeHeaders(EnvoyHeaders *headers) { + if (headers == nil) { + return envoy_noheaders; + } + envoy_header_size_t length = 0; for (NSString *headerKey in headers) { length += [headers[headerKey] count]; @@ -37,6 +55,16 @@ static inline envoy_headers toNativeHeaders(EnvoyHeaders *headers) { return ret; } +static inline envoy_headers *toNativeHeadersPtr(EnvoyHeaders *headers) { + if (headers == nil) { + return NULL; + } + + envoy_headers *ret = (envoy_headers *)safe_malloc(sizeof(envoy_headers)); + *ret = toNativeHeaders(headers); + return ret; +} + static inline NSData *to_ios_data(envoy_data data) { // TODO: we are copying from envoy_data to NSData. // https://github.com/lyft/envoy-mobile/issues/398 diff --git a/library/objective-c/EnvoyEngine.h b/library/objective-c/EnvoyEngine.h index 14266ef4cf..a0bf737f55 100644 --- a/library/objective-c/EnvoyEngine.h +++ b/library/objective-c/EnvoyEngine.h @@ -67,10 +67,12 @@ extern const int kEnvoyFilterHeadersStatusStopAllIterationAndBuffer; extern const int kEnvoyFilterDataStatusContinue; extern const int kEnvoyFilterDataStatusStopIterationAndBuffer; extern const int kEnvoyFilterDataStatusStopIterationNoBuffer; +extern const int kEnvoyFilterDataStatusResumeIteration; /// Return codes for on-trailers filter invocations. @see envoy/http/filter.h extern const int kEnvoyFilterTrailersStatusContinue; extern const int kEnvoyFilterTrailersStatusStopIteration; +extern const int kEnvoyFilterTrailersStatusResumeIteration; @interface EnvoyHTTPFilter : NSObject diff --git a/library/objective-c/EnvoyEngineImpl.m b/library/objective-c/EnvoyEngineImpl.m index 5848b5d78a..6f4ba57b74 100644 --- a/library/objective-c/EnvoyEngineImpl.m +++ b/library/objective-c/EnvoyEngineImpl.m @@ -51,7 +51,6 @@ static void ios_on_exit(void *context) { } EnvoyHeaders *platformHeaders = to_ios_headers(headers); - // TODO(goaway): consider better solution for compound return NSArray *result = filter.onResponseHeaders(platformHeaders, end_stream); return (envoy_filter_headers_status){/*status*/ [result[0] intValue], /*headers*/ toNativeHeaders(result[1])}; @@ -62,13 +61,15 @@ static envoy_filter_data_status ios_http_filter_on_request_data(envoy_data data, EnvoyHTTPFilter *filter = (__bridge EnvoyHTTPFilter *)context; if (filter.onRequestData == nil) { return (envoy_filter_data_status){/*status*/ kEnvoyFilterDataStatusContinue, - /*data*/ data}; + /*data*/ data, + /*pending_headers*/ NULL}; } NSData *platformData = to_ios_data(data); NSArray *result = filter.onRequestData(platformData, end_stream); return (envoy_filter_data_status){/*status*/ [result[0] intValue], - /*data*/ toNativeData(result[1])}; + /*data*/ toNativeData(result[1]), + /*pending_headers*/ toNativeHeadersPtr(result[2])}; } static envoy_filter_data_status ios_http_filter_on_response_data(envoy_data data, bool end_stream, @@ -76,13 +77,15 @@ static envoy_filter_data_status ios_http_filter_on_response_data(envoy_data data EnvoyHTTPFilter *filter = (__bridge EnvoyHTTPFilter *)context; if (filter.onResponseData == nil) { return (envoy_filter_data_status){/*status*/ kEnvoyFilterDataStatusContinue, - /*data*/ data}; + /*data*/ data, + /*pending_headers*/ NULL}; } NSData *platformData = to_ios_data(data); NSArray *result = filter.onResponseData(platformData, end_stream); return (envoy_filter_data_status){/*status*/ [result[0] intValue], - /*data*/ toNativeData(result[1])}; + /*data*/ toNativeData(result[1]), + /*pending_headers*/ toNativeHeadersPtr(result[2])}; } static envoy_filter_trailers_status ios_http_filter_on_request_trailers(envoy_headers trailers, @@ -90,13 +93,17 @@ static envoy_filter_trailers_status ios_http_filter_on_request_trailers(envoy_he EnvoyHTTPFilter *filter = (__bridge EnvoyHTTPFilter *)context; if (filter.onRequestTrailers == nil) { return (envoy_filter_trailers_status){/*status*/ kEnvoyFilterTrailersStatusContinue, - /*trailers*/ trailers}; + /*trailers*/ trailers, + /*pending_headers*/ NULL, + /*pending_trailers*/ NULL}; } EnvoyHeaders *platformTrailers = to_ios_headers(trailers); NSArray *result = filter.onRequestTrailers(platformTrailers); return (envoy_filter_trailers_status){/*status*/ [result[0] intValue], - /*trailers*/ toNativeHeaders(result[1])}; + /*trailers*/ toNativeHeaders(result[1]), + /*pending_headers*/ toNativeHeadersPtr(result[2]), + /*pending_data*/ toNativeDataPtr(result[3])}; } static envoy_filter_trailers_status ios_http_filter_on_response_trailers(envoy_headers trailers, @@ -104,13 +111,17 @@ static envoy_filter_trailers_status ios_http_filter_on_response_trailers(envoy_h EnvoyHTTPFilter *filter = (__bridge EnvoyHTTPFilter *)context; if (filter.onResponseTrailers == nil) { return (envoy_filter_trailers_status){/*status*/ kEnvoyFilterTrailersStatusContinue, - /*trailers*/ trailers}; + /*trailers*/ trailers, + /*pending_headers*/ NULL, + /*pending_data*/ NULL}; } EnvoyHeaders *platformTrailers = to_ios_headers(trailers); NSArray *result = filter.onResponseTrailers(platformTrailers); return (envoy_filter_trailers_status){/*status*/ [result[0] intValue], - /*trailers*/ toNativeHeaders(result[1])}; + /*trailers*/ toNativeHeaders(result[1]), + /*pending_headers*/ toNativeHeadersPtr(result[2]), + /*pending_data*/ toNativeDataPtr(result[3])}; } static void ios_http_filter_release(const void *context) { diff --git a/library/swift/src/filters/Filter.swift b/library/swift/src/filters/Filter.swift index 504fb015ed..0508da3516 100644 --- a/library/swift/src/filters/Filter.swift +++ b/library/swift/src/filters/Filter.swift @@ -39,8 +39,8 @@ extension EnvoyHTTPFilter { return [kEnvoyFilterDataStatusStopIterationAndBuffer, data] case .stopIterationNoBuffer: return [kEnvoyFilterDataStatusStopIterationNoBuffer, data] - case .resumeIteration(_, let data): - return [kEnvoyFilterDataStatusContinue, data] + case .resumeIteration(let headers, let data): + return [kEnvoyFilterDataStatusResumeIteration, headers?.headers as Any, data] } } @@ -51,8 +51,13 @@ extension EnvoyHTTPFilter { return [kEnvoyFilterTrailersStatusContinue, trailers.headers] case .stopIteration: return [kEnvoyFilterTrailersStatusStopIteration, envoyTrailers] - case .resumeIteration(_, _, let trailers): - return [kEnvoyFilterTrailersStatusContinue, trailers.headers] + case .resumeIteration(let headers, let data, let trailers): + return [ + kEnvoyFilterTrailersStatusResumeIteration, + headers?.headers as Any, + data as Any, + trailers.headers, + ] } } } @@ -78,8 +83,8 @@ extension EnvoyHTTPFilter { return [kEnvoyFilterDataStatusStopIterationAndBuffer, data] case .stopIterationNoBuffer: return [kEnvoyFilterDataStatusStopIterationNoBuffer, data] - case .resumeIteration(_, let data): - return [kEnvoyFilterDataStatusContinue, data] + case .resumeIteration(let headers, let data): + return [kEnvoyFilterDataStatusResumeIteration, headers?.headers as Any, data] } } @@ -90,8 +95,13 @@ extension EnvoyHTTPFilter { return [kEnvoyFilterTrailersStatusContinue, trailers.headers] case .stopIteration: return [kEnvoyFilterTrailersStatusStopIteration, envoyTrailers] - case .resumeIteration(_, _, let trailers): - return [kEnvoyFilterTrailersStatusContinue, trailers.headers] + case .resumeIteration(let headers, let data, let trailers): + return [ + kEnvoyFilterTrailersStatusResumeIteration, + headers?.headers as Any, + data as Any, + trailers.headers, + ] } } } diff --git a/test/common/extensions/filters/http/platform_bridge/platform_bridge_filter_test.cc b/test/common/extensions/filters/http/platform_bridge/platform_bridge_filter_test.cc index 6df17f453a..6a72ba15ec 100644 --- a/test/common/extensions/filters/http/platform_bridge/platform_bridge_filter_test.cc +++ b/test/common/extensions/filters/http/platform_bridge/platform_bridge_filter_test.cc @@ -3,6 +3,7 @@ #include "gtest/gtest.h" #include "library/common/api/external.h" +#include "library/common/buffer/utility.h" #include "library/common/extensions/filters/http/platform_bridge/filter.h" #include "library/common/extensions/filters/http/platform_bridge/filter.pb.h" @@ -19,6 +20,28 @@ std::string to_string(envoy_data data) { return std::string(reinterpret_cast(data.bytes), data.length); } +envoy_data make_envoy_data(const std::string& s) { + return copy_envoy_data(s.size(), reinterpret_cast(s.c_str())); +} + +envoy_headers make_envoy_headers(std::vector> pairs) { + envoy_header* headers = + static_cast(safe_malloc(sizeof(envoy_header) * pairs.size())); + envoy_headers new_headers; + new_headers.length = 0; + new_headers.headers = headers; + + for (const auto& pair : pairs) { + envoy_data key = make_envoy_data(pair.first); + envoy_data value = make_envoy_data(pair.second); + + new_headers.headers[new_headers.length] = {key, value}; + new_headers.length++; + } + + return new_headers; +} + class PlatformBridgeFilterTest : public testing::Test { public: void setUpFilter(std::string&& yaml, envoy_http_filter* platform_filter) { @@ -150,6 +173,60 @@ platform_filter_name: BasicContinueOnRequestHeaders EXPECT_EQ(invocations.on_request_headers_calls, 1); } +TEST_F(PlatformBridgeFilterTest, StopOnRequestHeadersThenResumeOnData) { + envoy_http_filter platform_filter; + filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; + platform_filter.static_context = &invocations; + platform_filter.init_filter = [](const void* context) -> const void* { + filter_invocations* invocations = static_cast(const_cast(context)); + invocations->init_filter_calls++; + return context; + }; + platform_filter.on_request_headers = [](envoy_headers c_headers, bool end_stream, + const void* context) -> envoy_filter_headers_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(c_headers.length, 1); + EXPECT_EQ(to_string(c_headers.headers[0].key), ":authority"); + EXPECT_EQ(to_string(c_headers.headers[0].value), "test.code"); + EXPECT_FALSE(end_stream); + invocations->on_request_headers_calls++; + release_envoy_headers(c_headers); + return {kEnvoyFilterHeadersStatusStopIteration, envoy_noheaders}; + }; + platform_filter.on_request_data = [](envoy_data c_data, bool end_stream, + const void* context) -> envoy_filter_data_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(to_string(c_data), "request body"); + EXPECT_TRUE(end_stream); + invocations->on_request_data_calls++; + envoy_headers* modified_headers = + static_cast(safe_malloc(sizeof(envoy_headers))); + *modified_headers = make_envoy_headers({{":authority", "test.code"}, {"content-length", "12"}}); + return {kEnvoyFilterDataStatusResumeIteration, c_data, modified_headers}; + }; + + setUpFilter(R"EOF( +platform_filter_name: StopOnRequestHeadersThenResumeOnData +)EOF", + &platform_filter); + EXPECT_EQ(invocations.init_filter_calls, 1); + + Http::TestRequestHeaderMapImpl request_headers{{":authority", "test.code"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ(invocations.on_request_headers_calls, 1); + + Buffer::OwnedImpl request_data = Buffer::OwnedImpl("request body"); + + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(request_data, true)); + EXPECT_EQ(invocations.on_request_data_calls, 1); + + EXPECT_TRUE(request_headers.get(Http::LowerCaseString("content-length"))); + EXPECT_EQ(request_headers.get(Http::LowerCaseString("content-length"))->value().getStringView(), + "12"); +} + TEST_F(PlatformBridgeFilterTest, BasicContinueOnRequestData) { envoy_http_filter platform_filter; filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; @@ -165,7 +242,7 @@ TEST_F(PlatformBridgeFilterTest, BasicContinueOnRequestData) { EXPECT_EQ(to_string(c_data), "request body"); EXPECT_TRUE(end_stream); invocations->on_request_data_calls++; - return {kEnvoyFilterDataStatusContinue, c_data}; + return {kEnvoyFilterDataStatusContinue, c_data, nullptr}; }; setUpFilter(R"EOF( @@ -196,7 +273,7 @@ TEST_F(PlatformBridgeFilterTest, StopAndBufferOnRequestData) { EXPECT_EQ(to_string(c_data), expected_data[invocations->on_request_data_calls++]); EXPECT_FALSE(end_stream); c_data.release(c_data.context); - return {kEnvoyFilterDataStatusStopIterationAndBuffer, envoy_nodata}; + return {kEnvoyFilterDataStatusStopIterationAndBuffer, envoy_nodata, nullptr}; }; Buffer::OwnedImpl decoding_buffer; @@ -234,6 +311,168 @@ platform_filter_name: StopAndBufferOnRequestData EXPECT_EQ(invocations.on_request_data_calls, 3); } +TEST_F(PlatformBridgeFilterTest, StopAndBufferThenResumeOnRequestData) { + envoy_http_filter platform_filter; + filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; + platform_filter.static_context = &invocations; + platform_filter.init_filter = [](const void* context) -> const void* { + filter_invocations* invocations = static_cast(const_cast(context)); + invocations->init_filter_calls++; + return context; + }; + platform_filter.on_request_data = [](envoy_data c_data, bool end_stream, + const void* context) -> envoy_filter_data_status { + filter_invocations* invocations = static_cast(const_cast(context)); + envoy_filter_data_status return_status; + + if (invocations->on_request_data_calls == 0) { + EXPECT_EQ(to_string(c_data), "A"); + EXPECT_FALSE(end_stream); + + return_status.status = kEnvoyFilterDataStatusStopIterationAndBuffer; + return_status.data = envoy_nodata; + return_status.pending_headers = nullptr; + } else { + EXPECT_EQ(to_string(c_data), "AB"); + EXPECT_FALSE(end_stream); + Buffer::OwnedImpl final_buffer = Buffer::OwnedImpl("C"); + envoy_data final_data = Buffer::Utility::toBridgeData(final_buffer); + + return_status.status = kEnvoyFilterDataStatusResumeIteration; + return_status.data = final_data; + return_status.pending_headers = nullptr; + } + + invocations->on_request_data_calls++; + c_data.release(c_data.context); + return return_status; + }; + + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(decoder_callbacks_, decodingBuffer()) + .Times(2) + .WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(decoder_callbacks_, modifyDecodingBuffer(_)) + .Times(2) + .WillRepeatedly(Invoke([&](std::function callback) -> void { + callback(decoding_buffer); + })); + + setUpFilter(R"EOF( +platform_filter_name: StopAndBufferThenResumeOnRequestData +)EOF", + &platform_filter); + EXPECT_EQ(invocations.init_filter_calls, 1); + + Buffer::OwnedImpl first_chunk = Buffer::OwnedImpl("A"); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, + filter_->decodeData(first_chunk, false)); + // Since the return code can't be handled in a unit test, manually update the buffer here. + decoding_buffer.move(first_chunk); + EXPECT_EQ(invocations.on_request_data_calls, 1); + + Buffer::OwnedImpl second_chunk = Buffer::OwnedImpl("B"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(second_chunk, false)); + // Manual update not required, because once iteration is stopped, data is added directly. + EXPECT_EQ(invocations.on_request_data_calls, 2); + // Buffer has been updated with value from ResumeIteration. + EXPECT_EQ(decoding_buffer.toString(), "C"); +} + +TEST_F(PlatformBridgeFilterTest, StopOnRequestHeadersThenBufferThenResumeOnData) { + envoy_http_filter platform_filter; + filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; + platform_filter.static_context = &invocations; + platform_filter.init_filter = [](const void* context) -> const void* { + filter_invocations* invocations = static_cast(const_cast(context)); + invocations->init_filter_calls++; + return context; + }; + platform_filter.on_request_headers = [](envoy_headers c_headers, bool end_stream, + const void* context) -> envoy_filter_headers_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(c_headers.length, 1); + EXPECT_EQ(to_string(c_headers.headers[0].key), ":authority"); + EXPECT_EQ(to_string(c_headers.headers[0].value), "test.code"); + EXPECT_FALSE(end_stream); + invocations->on_request_headers_calls++; + release_envoy_headers(c_headers); + return {kEnvoyFilterHeadersStatusStopIteration, envoy_noheaders}; + }; + platform_filter.on_request_data = [](envoy_data c_data, bool end_stream, + const void* context) -> envoy_filter_data_status { + filter_invocations* invocations = static_cast(const_cast(context)); + envoy_filter_data_status return_status; + + if (invocations->on_request_data_calls == 0) { + EXPECT_EQ(to_string(c_data), "A"); + EXPECT_FALSE(end_stream); + + return_status.status = kEnvoyFilterDataStatusStopIterationAndBuffer; + return_status.data = envoy_nodata; + return_status.pending_headers = nullptr; + } else { + EXPECT_EQ(to_string(c_data), "AB"); + EXPECT_TRUE(end_stream); + Buffer::OwnedImpl final_buffer = Buffer::OwnedImpl("C"); + envoy_data final_data = Buffer::Utility::toBridgeData(final_buffer); + envoy_headers* modified_headers = + static_cast(safe_malloc(sizeof(envoy_headers))); + *modified_headers = + make_envoy_headers({{":authority", "test.code"}, {"content-length", "1"}}); + + return_status.status = kEnvoyFilterDataStatusResumeIteration; + return_status.data = final_data; + return_status.pending_headers = modified_headers; + } + + invocations->on_request_data_calls++; + c_data.release(c_data.context); + return return_status; + }; + + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(decoder_callbacks_, decodingBuffer()) + .Times(2) + .WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(decoder_callbacks_, modifyDecodingBuffer(_)) + .Times(2) + .WillRepeatedly(Invoke([&](std::function callback) -> void { + callback(decoding_buffer); + })); + + setUpFilter(R"EOF( +platform_filter_name: StopOnRequestHeadersThenBufferThenResumeOnData +)EOF", + &platform_filter); + EXPECT_EQ(invocations.init_filter_calls, 1); + + Http::TestRequestHeaderMapImpl request_headers{{":authority", "test.code"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ(invocations.on_request_headers_calls, 1); + + Buffer::OwnedImpl first_chunk = Buffer::OwnedImpl("A"); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, + filter_->decodeData(first_chunk, false)); + // Since the return code can't be handled in a unit test, manually update the buffer here. + decoding_buffer.move(first_chunk); + EXPECT_EQ(invocations.on_request_data_calls, 1); + + Buffer::OwnedImpl second_chunk = Buffer::OwnedImpl("B"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(second_chunk, true)); + // Manual update not required, because once iteration is stopped, data is added directly. + EXPECT_EQ(invocations.on_request_data_calls, 2); + // Buffer has been updated with value from ResumeIteration. + EXPECT_EQ(decoding_buffer.toString(), "C"); + + // Pending headers have been updated with value from ResumeIteration. + EXPECT_TRUE(request_headers.get(Http::LowerCaseString("content-length"))); + EXPECT_EQ(request_headers.get(Http::LowerCaseString("content-length"))->value().getStringView(), + "1"); +} + TEST_F(PlatformBridgeFilterTest, StopNoBufferOnRequestData) { envoy_http_filter platform_filter; filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; @@ -250,7 +489,7 @@ TEST_F(PlatformBridgeFilterTest, StopNoBufferOnRequestData) { EXPECT_EQ(to_string(c_data), expected_data[invocations->on_request_data_calls++]); EXPECT_FALSE(end_stream); c_data.release(c_data.context); - return {kEnvoyFilterDataStatusStopIterationNoBuffer, envoy_nodata}; + return {kEnvoyFilterDataStatusStopIterationNoBuffer, envoy_nodata, nullptr}; }; setUpFilter(R"EOF( @@ -289,7 +528,7 @@ TEST_F(PlatformBridgeFilterTest, BasicContinueOnRequestTrailers) { EXPECT_EQ(to_string(c_trailers.headers[0].key), "x-test-trailer"); EXPECT_EQ(to_string(c_trailers.headers[0].value), "test trailer"); invocations->on_request_trailers_calls++; - return {kEnvoyFilterTrailersStatusContinue, c_trailers}; + return {kEnvoyFilterTrailersStatusContinue, c_trailers, nullptr, nullptr}; }; setUpFilter(R"EOF( @@ -304,6 +543,104 @@ platform_filter_name: BasicContinueOnRequestTrailers EXPECT_EQ(invocations.on_request_trailers_calls, 1); } +TEST_F(PlatformBridgeFilterTest, StopOnRequestHeadersThenBufferThenResumeOnTrailers) { + envoy_http_filter platform_filter; + filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; + platform_filter.static_context = &invocations; + platform_filter.init_filter = [](const void* context) -> const void* { + filter_invocations* invocations = static_cast(const_cast(context)); + invocations->init_filter_calls++; + return context; + }; + platform_filter.on_request_headers = [](envoy_headers c_headers, bool end_stream, + const void* context) -> envoy_filter_headers_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(c_headers.length, 1); + EXPECT_EQ(to_string(c_headers.headers[0].key), ":authority"); + EXPECT_EQ(to_string(c_headers.headers[0].value), "test.code"); + EXPECT_FALSE(end_stream); + invocations->on_request_headers_calls++; + release_envoy_headers(c_headers); + return {kEnvoyFilterHeadersStatusStopIteration, envoy_noheaders}; + }; + platform_filter.on_request_data = [](envoy_data c_data, bool end_stream, + const void* context) -> envoy_filter_data_status { + filter_invocations* invocations = static_cast(const_cast(context)); + std::string expected_data[2] = {"A", "AB"}; + EXPECT_EQ(to_string(c_data), expected_data[invocations->on_request_data_calls]); + EXPECT_FALSE(end_stream); + c_data.release(c_data.context); + invocations->on_request_data_calls++; + return {kEnvoyFilterDataStatusStopIterationAndBuffer, envoy_nodata, nullptr}; + }; + platform_filter.on_request_trailers = [](envoy_headers c_trailers, + const void* context) -> envoy_filter_trailers_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(c_trailers.length, 1); + EXPECT_EQ(to_string(c_trailers.headers[0].key), "x-test-trailer"); + EXPECT_EQ(to_string(c_trailers.headers[0].value), "test trailer"); + + Buffer::OwnedImpl final_buffer = Buffer::OwnedImpl("C"); + envoy_data* modified_data = static_cast(safe_malloc(sizeof(envoy_data))); + *modified_data = Buffer::Utility::toBridgeData(final_buffer); + envoy_headers* modified_headers = + static_cast(safe_malloc(sizeof(envoy_headers))); + *modified_headers = make_envoy_headers({{":authority", "test.code"}, {"content-length", "1"}}); + + invocations->on_request_trailers_calls++; + return {kEnvoyFilterTrailersStatusResumeIteration, c_trailers, modified_headers, modified_data}; + }; + + Buffer::OwnedImpl decoding_buffer; + EXPECT_CALL(decoder_callbacks_, decodingBuffer()) + .Times(3) + .WillRepeatedly(Return(&decoding_buffer)); + EXPECT_CALL(decoder_callbacks_, modifyDecodingBuffer(_)) + .Times(3) + .WillRepeatedly(Invoke([&](std::function callback) -> void { + callback(decoding_buffer); + })); + + setUpFilter(R"EOF( +platform_filter_name: StopOnRequestHeadersThenBufferThenResumeOnTrailers +)EOF", + &platform_filter); + EXPECT_EQ(invocations.init_filter_calls, 1); + + Http::TestRequestHeaderMapImpl request_headers{{":authority", "test.code"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ(invocations.on_request_headers_calls, 1); + + Buffer::OwnedImpl first_chunk = Buffer::OwnedImpl("A"); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, + filter_->decodeData(first_chunk, false)); + // Since the return code can't be handled in a unit test, manually update the buffer here. + decoding_buffer.move(first_chunk); + EXPECT_EQ(invocations.on_request_data_calls, 1); + + Buffer::OwnedImpl second_chunk = Buffer::OwnedImpl("B"); + EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, + filter_->decodeData(second_chunk, false)); + // Manual update not required, because once iteration is stopped, data is added directly. + EXPECT_EQ(invocations.on_request_data_calls, 2); + EXPECT_EQ(decoding_buffer.toString(), "AB"); + + Http::TestRequestTrailerMapImpl request_trailers{{"x-test-trailer", "test trailer"}}; + + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); + EXPECT_EQ(invocations.on_request_trailers_calls, 1); + + // Buffer has been updated with value from ResumeIteration. + EXPECT_EQ(decoding_buffer.toString(), "C"); + + // Pending headers have been updated with value from ResumeIteration. + EXPECT_TRUE(request_headers.get(Http::LowerCaseString("content-length"))); + EXPECT_EQ(request_headers.get(Http::LowerCaseString("content-length"))->value().getStringView(), + "1"); +} + // DIVIDE TEST_F(PlatformBridgeFilterTest, BasicContinueOnResponseHeaders) { @@ -338,6 +675,60 @@ platform_filter_name: BasicContinueOnResponseHeaders EXPECT_EQ(invocations.on_response_headers_calls, 1); } +TEST_F(PlatformBridgeFilterTest, StopOnResponseHeadersThenResumeOnData) { + envoy_http_filter platform_filter; + filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; + platform_filter.static_context = &invocations; + platform_filter.init_filter = [](const void* context) -> const void* { + filter_invocations* invocations = static_cast(const_cast(context)); + invocations->init_filter_calls++; + return context; + }; + platform_filter.on_response_headers = [](envoy_headers c_headers, bool end_stream, + const void* context) -> envoy_filter_headers_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(c_headers.length, 1); + EXPECT_EQ(to_string(c_headers.headers[0].key), ":status"); + EXPECT_EQ(to_string(c_headers.headers[0].value), "test.code"); + EXPECT_FALSE(end_stream); + invocations->on_response_headers_calls++; + release_envoy_headers(c_headers); + return {kEnvoyFilterHeadersStatusStopIteration, envoy_noheaders}; + }; + platform_filter.on_response_data = [](envoy_data c_data, bool end_stream, + const void* context) -> envoy_filter_data_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(to_string(c_data), "response body"); + EXPECT_TRUE(end_stream); + invocations->on_response_data_calls++; + envoy_headers* modified_headers = + static_cast(safe_malloc(sizeof(envoy_headers))); + *modified_headers = make_envoy_headers({{":status", "test.code"}, {"content-length", "13"}}); + return {kEnvoyFilterDataStatusResumeIteration, c_data, modified_headers}; + }; + + setUpFilter(R"EOF( +platform_filter_name: StopOnResponseHeadersThenResumeOnData +)EOF", + &platform_filter); + EXPECT_EQ(invocations.init_filter_calls, 1); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "test.code"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->encodeHeaders(response_headers, false)); + EXPECT_EQ(invocations.on_response_headers_calls, 1); + + Buffer::OwnedImpl response_data = Buffer::OwnedImpl("response body"); + + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(response_data, true)); + EXPECT_EQ(invocations.on_response_data_calls, 1); + + EXPECT_TRUE(response_headers.get(Http::LowerCaseString("content-length"))); + EXPECT_EQ(response_headers.get(Http::LowerCaseString("content-length"))->value().getStringView(), + "13"); +} + TEST_F(PlatformBridgeFilterTest, BasicContinueOnResponseData) { envoy_http_filter platform_filter; filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; @@ -353,7 +744,7 @@ TEST_F(PlatformBridgeFilterTest, BasicContinueOnResponseData) { EXPECT_EQ(to_string(c_data), "response body"); EXPECT_TRUE(end_stream); invocations->on_response_data_calls++; - return {kEnvoyFilterDataStatusContinue, c_data}; + return {kEnvoyFilterDataStatusContinue, c_data, nullptr}; }; setUpFilter(R"EOF( @@ -384,7 +775,7 @@ TEST_F(PlatformBridgeFilterTest, StopAndBufferOnResponseData) { EXPECT_EQ(to_string(c_data), expected_data[invocations->on_response_data_calls++]); EXPECT_FALSE(end_stream); c_data.release(c_data.context); - return {kEnvoyFilterDataStatusStopIterationAndBuffer, envoy_nodata}; + return {kEnvoyFilterDataStatusStopIterationAndBuffer, envoy_nodata, nullptr}; }; Buffer::OwnedImpl encoding_buffer; @@ -422,6 +813,167 @@ platform_filter_name: StopAndBufferOnResponseData EXPECT_EQ(invocations.on_response_data_calls, 3); } +TEST_F(PlatformBridgeFilterTest, StopAndBufferThenResumeOnResponseData) { + envoy_http_filter platform_filter; + filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; + platform_filter.static_context = &invocations; + platform_filter.init_filter = [](const void* context) -> const void* { + filter_invocations* invocations = static_cast(const_cast(context)); + invocations->init_filter_calls++; + return context; + }; + platform_filter.on_response_data = [](envoy_data c_data, bool end_stream, + const void* context) -> envoy_filter_data_status { + filter_invocations* invocations = static_cast(const_cast(context)); + envoy_filter_data_status return_status; + + if (invocations->on_response_data_calls == 0) { + EXPECT_EQ(to_string(c_data), "A"); + EXPECT_FALSE(end_stream); + + return_status.status = kEnvoyFilterDataStatusStopIterationAndBuffer; + return_status.data = envoy_nodata; + return_status.pending_headers = nullptr; + } else { + EXPECT_EQ(to_string(c_data), "AB"); + EXPECT_FALSE(end_stream); + Buffer::OwnedImpl final_buffer = Buffer::OwnedImpl("C"); + envoy_data final_data = Buffer::Utility::toBridgeData(final_buffer); + + return_status.status = kEnvoyFilterDataStatusResumeIteration; + return_status.data = final_data; + return_status.pending_headers = nullptr; + } + + invocations->on_response_data_calls++; + c_data.release(c_data.context); + return return_status; + }; + + Buffer::OwnedImpl encoding_buffer; + EXPECT_CALL(encoder_callbacks_, encodingBuffer()) + .Times(2) + .WillRepeatedly(Return(&encoding_buffer)); + EXPECT_CALL(encoder_callbacks_, modifyEncodingBuffer(_)) + .Times(2) + .WillRepeatedly(Invoke([&](std::function callback) -> void { + callback(encoding_buffer); + })); + + setUpFilter(R"EOF( +platform_filter_name: StopAndBufferThenResumeOnResponseData +)EOF", + &platform_filter); + EXPECT_EQ(invocations.init_filter_calls, 1); + + Buffer::OwnedImpl first_chunk = Buffer::OwnedImpl("A"); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, + filter_->encodeData(first_chunk, false)); + // Since the return code can't be handled in a unit test, manually update the buffer here. + encoding_buffer.move(first_chunk); + EXPECT_EQ(invocations.on_response_data_calls, 1); + + Buffer::OwnedImpl second_chunk = Buffer::OwnedImpl("B"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(second_chunk, false)); + // Manual update not required, because once iteration is stopped, data is added directly. + EXPECT_EQ(invocations.on_response_data_calls, 2); + // Buffer has been updated with value from ResumeIteration. + EXPECT_EQ(encoding_buffer.toString(), "C"); +} + +TEST_F(PlatformBridgeFilterTest, StopOnResponseHeadersThenBufferThenResumeOnData) { + envoy_http_filter platform_filter; + filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; + platform_filter.static_context = &invocations; + platform_filter.init_filter = [](const void* context) -> const void* { + filter_invocations* invocations = static_cast(const_cast(context)); + invocations->init_filter_calls++; + return context; + }; + platform_filter.on_response_headers = [](envoy_headers c_headers, bool end_stream, + const void* context) -> envoy_filter_headers_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(c_headers.length, 1); + EXPECT_EQ(to_string(c_headers.headers[0].key), ":status"); + EXPECT_EQ(to_string(c_headers.headers[0].value), "test.code"); + EXPECT_FALSE(end_stream); + invocations->on_response_headers_calls++; + release_envoy_headers(c_headers); + return {kEnvoyFilterHeadersStatusStopIteration, envoy_noheaders}; + }; + platform_filter.on_response_data = [](envoy_data c_data, bool end_stream, + const void* context) -> envoy_filter_data_status { + filter_invocations* invocations = static_cast(const_cast(context)); + envoy_filter_data_status return_status; + + if (invocations->on_response_data_calls == 0) { + EXPECT_EQ(to_string(c_data), "A"); + EXPECT_FALSE(end_stream); + + return_status.status = kEnvoyFilterDataStatusStopIterationAndBuffer; + return_status.data = envoy_nodata; + return_status.pending_headers = nullptr; + } else { + EXPECT_EQ(to_string(c_data), "AB"); + EXPECT_TRUE(end_stream); + Buffer::OwnedImpl final_buffer = Buffer::OwnedImpl("C"); + envoy_data final_data = Buffer::Utility::toBridgeData(final_buffer); + envoy_headers* modified_headers = + static_cast(safe_malloc(sizeof(envoy_headers))); + *modified_headers = make_envoy_headers({{":status", "test.code"}, {"content-length", "1"}}); + + return_status.status = kEnvoyFilterDataStatusResumeIteration; + return_status.data = final_data; + return_status.pending_headers = modified_headers; + } + + invocations->on_response_data_calls++; + c_data.release(c_data.context); + return return_status; + }; + + Buffer::OwnedImpl encoding_buffer; + EXPECT_CALL(encoder_callbacks_, encodingBuffer()) + .Times(2) + .WillRepeatedly(Return(&encoding_buffer)); + EXPECT_CALL(encoder_callbacks_, modifyEncodingBuffer(_)) + .Times(2) + .WillRepeatedly(Invoke([&](std::function callback) -> void { + callback(encoding_buffer); + })); + + setUpFilter(R"EOF( +platform_filter_name: StopOnResponseHeadersThenBufferThenResumeOnData +)EOF", + &platform_filter); + EXPECT_EQ(invocations.init_filter_calls, 1); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "test.code"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->encodeHeaders(response_headers, false)); + EXPECT_EQ(invocations.on_response_headers_calls, 1); + + Buffer::OwnedImpl first_chunk = Buffer::OwnedImpl("A"); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, + filter_->encodeData(first_chunk, false)); + // Since the return code can't be handled in a unit test, manually update the buffer here. + encoding_buffer.move(first_chunk); + EXPECT_EQ(invocations.on_response_data_calls, 1); + + Buffer::OwnedImpl second_chunk = Buffer::OwnedImpl("B"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(second_chunk, true)); + // Manual update not required, because once iteration is stopped, data is added directly. + EXPECT_EQ(invocations.on_response_data_calls, 2); + // Buffer has been updated with value from ResumeIteration. + EXPECT_EQ(encoding_buffer.toString(), "C"); + + // Pending headers have been updated with value from ResumeIteration. + EXPECT_TRUE(response_headers.get(Http::LowerCaseString("content-length"))); + EXPECT_EQ(response_headers.get(Http::LowerCaseString("content-length"))->value().getStringView(), + "1"); +} + TEST_F(PlatformBridgeFilterTest, StopNoBufferOnResponseData) { envoy_http_filter platform_filter; filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; @@ -438,7 +990,7 @@ TEST_F(PlatformBridgeFilterTest, StopNoBufferOnResponseData) { EXPECT_EQ(to_string(c_data), expected_data[invocations->on_response_data_calls++]); EXPECT_FALSE(end_stream); c_data.release(c_data.context); - return {kEnvoyFilterDataStatusStopIterationNoBuffer, envoy_nodata}; + return {kEnvoyFilterDataStatusStopIterationNoBuffer, envoy_nodata, nullptr}; }; setUpFilter(R"EOF( @@ -477,7 +1029,7 @@ TEST_F(PlatformBridgeFilterTest, BasicContinueOnResponseTrailers) { EXPECT_EQ(to_string(c_trailers.headers[0].key), "x-test-trailer"); EXPECT_EQ(to_string(c_trailers.headers[0].value), "test trailer"); invocations->on_response_trailers_calls++; - return {kEnvoyFilterTrailersStatusContinue, c_trailers}; + return {kEnvoyFilterTrailersStatusContinue, c_trailers, nullptr, nullptr}; }; setUpFilter(R"EOF( @@ -492,6 +1044,104 @@ platform_filter_name: BasicContinueOnResponseTrailers EXPECT_EQ(invocations.on_response_trailers_calls, 1); } +TEST_F(PlatformBridgeFilterTest, StopOnResponseHeadersThenBufferThenResumeOnTrailers) { + envoy_http_filter platform_filter; + filter_invocations invocations = {0, 0, 0, 0, 0, 0, 0, 0}; + platform_filter.static_context = &invocations; + platform_filter.init_filter = [](const void* context) -> const void* { + filter_invocations* invocations = static_cast(const_cast(context)); + invocations->init_filter_calls++; + return context; + }; + platform_filter.on_response_headers = [](envoy_headers c_headers, bool end_stream, + const void* context) -> envoy_filter_headers_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(c_headers.length, 1); + EXPECT_EQ(to_string(c_headers.headers[0].key), ":status"); + EXPECT_EQ(to_string(c_headers.headers[0].value), "test.code"); + EXPECT_FALSE(end_stream); + invocations->on_response_headers_calls++; + release_envoy_headers(c_headers); + return {kEnvoyFilterHeadersStatusStopIteration, envoy_noheaders}; + }; + platform_filter.on_response_data = [](envoy_data c_data, bool end_stream, + const void* context) -> envoy_filter_data_status { + filter_invocations* invocations = static_cast(const_cast(context)); + std::string expected_data[2] = {"A", "AB"}; + EXPECT_EQ(to_string(c_data), expected_data[invocations->on_response_data_calls]); + EXPECT_FALSE(end_stream); + c_data.release(c_data.context); + invocations->on_response_data_calls++; + return {kEnvoyFilterDataStatusStopIterationAndBuffer, envoy_nodata, nullptr}; + }; + platform_filter.on_response_trailers = [](envoy_headers c_trailers, + const void* context) -> envoy_filter_trailers_status { + filter_invocations* invocations = static_cast(const_cast(context)); + EXPECT_EQ(c_trailers.length, 1); + EXPECT_EQ(to_string(c_trailers.headers[0].key), "x-test-trailer"); + EXPECT_EQ(to_string(c_trailers.headers[0].value), "test trailer"); + + Buffer::OwnedImpl final_buffer = Buffer::OwnedImpl("C"); + envoy_data* modified_data = static_cast(safe_malloc(sizeof(envoy_data))); + *modified_data = Buffer::Utility::toBridgeData(final_buffer); + envoy_headers* modified_headers = + static_cast(safe_malloc(sizeof(envoy_headers))); + *modified_headers = make_envoy_headers({{":status", "test.code"}, {"content-length", "1"}}); + + invocations->on_response_trailers_calls++; + return {kEnvoyFilterTrailersStatusResumeIteration, c_trailers, modified_headers, modified_data}; + }; + + Buffer::OwnedImpl encoding_buffer; + EXPECT_CALL(encoder_callbacks_, encodingBuffer()) + .Times(3) + .WillRepeatedly(Return(&encoding_buffer)); + EXPECT_CALL(encoder_callbacks_, modifyEncodingBuffer(_)) + .Times(3) + .WillRepeatedly(Invoke([&](std::function callback) -> void { + callback(encoding_buffer); + })); + + setUpFilter(R"EOF( +platform_filter_name: StopOnResponseHeadersThenBufferThenResumeOnTrailers +)EOF", + &platform_filter); + EXPECT_EQ(invocations.init_filter_calls, 1); + + Http::TestResponseHeaderMapImpl response_headers{{":status", "test.code"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->encodeHeaders(response_headers, false)); + EXPECT_EQ(invocations.on_response_headers_calls, 1); + + Buffer::OwnedImpl first_chunk = Buffer::OwnedImpl("A"); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, + filter_->encodeData(first_chunk, false)); + // Since the return code can't be handled in a unit test, manually update the buffer here. + encoding_buffer.move(first_chunk); + EXPECT_EQ(invocations.on_response_data_calls, 1); + + Buffer::OwnedImpl second_chunk = Buffer::OwnedImpl("B"); + EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, + filter_->encodeData(second_chunk, false)); + // Manual update not required, because once iteration is stopped, data is added directly. + EXPECT_EQ(invocations.on_response_data_calls, 2); + EXPECT_EQ(encoding_buffer.toString(), "AB"); + + Http::TestResponseTrailerMapImpl response_trailers{{"x-test-trailer", "test trailer"}}; + + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers)); + EXPECT_EQ(invocations.on_response_trailers_calls, 1); + + // Buffer has been updated with value from ResumeIteration. + EXPECT_EQ(encoding_buffer.toString(), "C"); + + // Pending headers have been updated with value from ResumeIteration. + EXPECT_TRUE(response_headers.get(Http::LowerCaseString("content-length"))); + EXPECT_EQ(response_headers.get(Http::LowerCaseString("content-length"))->value().getStringView(), + "1"); +} + } // namespace } // namespace PlatformBridge } // namespace HttpFilters