diff --git a/source/extensions/filters/http/ext_proc/http_client/BUILD b/source/extensions/filters/http/ext_proc/http_client/BUILD new file mode 100644 index 000000000000..16afa5f69849 --- /dev/null +++ b/source/extensions/filters/http/ext_proc/http_client/BUILD @@ -0,0 +1,29 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "client_base_interface", + hdrs = ["client_base.h"], + tags = ["skip_on_windows"], + deps = [], +) + +envoy_cc_library( + name = "http_client_lib", + srcs = ["http_client_impl.cc"], + hdrs = ["http_client_impl.h"], + tags = ["skip_on_windows"], + deps = [ + "client_base_interface", + "//source/common/common:enum_to_int", + "//source/common/http:utility_lib", + "//source/extensions/filters/http/ext_proc", + ], +) diff --git a/source/extensions/filters/http/ext_proc/http_client/client_base.h b/source/extensions/filters/http/ext_proc/http_client/client_base.h new file mode 100644 index 000000000000..fd9ae5ce7c7f --- /dev/null +++ b/source/extensions/filters/http/ext_proc/http_client/client_base.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +/** + * Async callbacks used during external processing. + */ +class RequestCallbacks { +public: + virtual ~RequestCallbacks() = default; + virtual void onComplete() PURE; +}; + +/** + * Async client base class used during external processing. + */ +class ClientBase { +public: + virtual ~ClientBase() = default; + + virtual void sendRequest() PURE; + virtual void cancel() PURE; +}; + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/ext_proc/http_client/http_client_impl.cc b/source/extensions/filters/http/ext_proc/http_client/http_client_impl.cc new file mode 100644 index 000000000000..abc172192389 --- /dev/null +++ b/source/extensions/filters/http/ext_proc/http_client/http_client_impl.cc @@ -0,0 +1,45 @@ +#include "source/extensions/filters/http/ext_proc/http_client/http_client_impl.h" + +#include "source/common/common/enum_to_int.h" +#include "source/common/http/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +void ExtProcHttpClient::onSuccess(const Http::AsyncClient::Request&, + Http::ResponseMessagePtr&& response) { + auto status = Envoy::Http::Utility::getResponseStatusOrNullopt(response->headers()); + if (status.has_value()) { + uint64_t status_code = status.value(); + if (status_code == Envoy::enumToInt(Envoy::Http::Code::OK)) { + ENVOY_LOG(error, "Response status is OK"); + } else { + ENVOY_LOG(error, "Response status is not OK, status: {}", status_code); + onError(); + } + } else { + // This occurs if the response headers are invalid. + ENVOY_LOG(error, "Failed to get the response because response headers are not valid."); + onError(); + } +} + +void ExtProcHttpClient::onFailure(const Http::AsyncClient::Request&, + Http::AsyncClient::FailureReason reason) { + ASSERT(reason == Http::AsyncClient::FailureReason::Reset || + reason == Http::AsyncClient::FailureReason::ExceedResponseBufferLimit); + ENVOY_LOG(error, "Request failed: stream has been reset"); + onError(); +} + +void ExtProcHttpClient::onError() { + // Cancel if the request is active. + cancel(); +} + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/ext_proc/http_client/http_client_impl.h b/source/extensions/filters/http/ext_proc/http_client/http_client_impl.h new file mode 100644 index 000000000000..fa2df5afd12d --- /dev/null +++ b/source/extensions/filters/http/ext_proc/http_client/http_client_impl.h @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include "envoy/http/async_client.h" + +#include "source/common/common/logger.h" +#include "source/extensions/filters/http/ext_proc/ext_proc.h" +#include "source/extensions/filters/http/ext_proc/http_client/client_base.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +class ExtProcHttpClient : public ClientBase, + public Http::AsyncClient::Callbacks, + public Logger::Loggable { +public: + ExtProcHttpClient(const envoy::extensions::filters::http::ext_proc::v3::ExternalProcessor& config, + Server::Configuration::ServerFactoryContext& context) + : config_(config), context_(context) {} + + ~ExtProcHttpClient() { cancel(); } + + void sendRequest() override {} + void cancel() override {} + void onBeforeFinalizeUpstreamSpan(Tracing::Span&, const Http::ResponseHeaderMap*) override {} + + // Http::AsyncClient::Callbacks implemented by this class. + void onSuccess(const Http::AsyncClient::Request& request, + Http::ResponseMessagePtr&& response) override; + void onFailure(const Http::AsyncClient::Request& request, + Http::AsyncClient::FailureReason reason) override; + + Server::Configuration::ServerFactoryContext& context() const { return context_; } + +private: + void onError(); + envoy::extensions::filters::http::ext_proc::v3::ExternalProcessor config_; + Server::Configuration::ServerFactoryContext& context_; +}; + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/ext_proc/http_client/BUILD b/test/extensions/filters/http/ext_proc/http_client/BUILD new file mode 100644 index 000000000000..d5c2826982dd --- /dev/null +++ b/test/extensions/filters/http/ext_proc/http_client/BUILD @@ -0,0 +1,26 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "http_client_test", + size = "small", + srcs = ["http_client_test.cc"], + extension_names = ["envoy.filters.http.ext_proc"], + tags = ["skip_on_windows"], + deps = [ + "//source/common/http:message_lib", + "//source/extensions/filters/http/ext_proc/http_client:http_client_lib", + "//test/mocks/server:factory_context_mocks", + "@envoy_api//envoy/extensions/filters/http/ext_proc/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/http/ext_proc/http_client/http_client_test.cc b/test/extensions/filters/http/ext_proc/http_client/http_client_test.cc new file mode 100644 index 000000000000..27a6d83c9ef1 --- /dev/null +++ b/test/extensions/filters/http/ext_proc/http_client/http_client_test.cc @@ -0,0 +1,65 @@ +#include "envoy/extensions/filters/http/ext_proc/v3/ext_proc.pb.h" + +#include "source/common/http/message_impl.h" +#include "source/extensions/filters/http/ext_proc/http_client/http_client_impl.h" + +#include "test/mocks/server/server_factory_context.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { +namespace { + +class ExtProcHttpClientTest : public testing::Test { +public: + ~ExtProcHttpClientTest() override = default; + + void SetUp() override { client_ = std::make_unique(config_, context_); } + +protected: + envoy::extensions::filters::http::ext_proc::v3::ExternalProcessor config_; + testing::NiceMock context_; + Upstream::MockClusterManager& cm_{context_.cluster_manager_}; + std::unique_ptr client_; + testing::NiceMock async_request_{ + &cm_.thread_local_cluster_.async_client_}; +}; + +TEST_F(ExtProcHttpClientTest, Basic) { + SetUp(); + client_->sendRequest(); + client_->cancel(); + client_->context(); + Tracing::MockSpan parent_span; + client_->onBeforeFinalizeUpstreamSpan(parent_span, nullptr); + Http::AsyncClient::FailureReason reason = Envoy::Http::AsyncClient::FailureReason::Reset; + client_->onFailure(async_request_, reason); + + Http::ResponseHeaderMapPtr resp_headers_ok(new Http::TestResponseHeaderMapImpl({ + {":status", "200"}, + })); + Http::ResponseMessagePtr response_ok(new Http::ResponseMessageImpl(std::move(resp_headers_ok))); + client_->onSuccess(async_request_, std::move(response_ok)); + + Http::ResponseHeaderMapPtr resp_headers(new Http::TestResponseHeaderMapImpl({ + {":status", "403"}, + })); + Http::ResponseMessagePtr response(new Http::ResponseMessageImpl(std::move(resp_headers))); + client_->onSuccess(async_request_, std::move(response)); + + Http::ResponseHeaderMapPtr resp_headers_foo(new Http::TestResponseHeaderMapImpl({ + {":status", "foo"}, + })); + Http::ResponseMessagePtr response_foo(new Http::ResponseMessageImpl(std::move(resp_headers_foo))); + client_->onSuccess(async_request_, std::move(response_foo)); +} + +} // namespace +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy