From 7eda4d0be3ec6d2ecb98bd5dfee584ace493c6e7 Mon Sep 17 00:00:00 2001 From: Yakov Olkhovskiy <99031427+yakov-olkhovskiy@users.noreply.github.com> Date: Wed, 28 May 2025 07:05:20 +0000 Subject: [PATCH 1/5] Merge pull request #79975 from zvonand/add-headers-in-handlers Allow to add `http_response_headers` in `http_handlers` of any type --- docs/en/interfaces/http.md | 37 ++++- src/Server/HTTPHandler.cpp | 10 +- src/Server/HTTPHandlerFactory.cpp | 104 +++++++++++-- src/Server/HTTPHandlerFactory.h | 12 +- src/Server/HTTPResponseHeaderWriter.cpp | 21 +++ src/Server/HTTPResponseHeaderWriter.h | 12 ++ src/Server/PrometheusRequestHandler.cpp | 5 +- src/Server/PrometheusRequestHandler.h | 4 +- .../PrometheusRequestHandlerFactory.cpp | 14 +- src/Server/PrometheusRequestHandlerFactory.h | 3 +- src/Server/ReplicasStatusHandler.cpp | 14 +- src/Server/ReplicasStatusHandler.h | 10 ++ src/Server/StaticRequestHandler.cpp | 5 +- src/Server/WebUIRequestHandler.cpp | 22 ++- src/Server/WebUIRequestHandler.h | 47 ++++++ .../test_http_handlers_config/test.py | 28 ++++ .../test_headers_in_response/config.xml | 146 ++++++++++++++++++ 17 files changed, 452 insertions(+), 42 deletions(-) create mode 100644 tests/integration/test_http_handlers_config/test_headers_in_response/config.xml diff --git a/docs/en/interfaces/http.md b/docs/en/interfaces/http.md index 03fdfa048c8d..44e460dadbfb 100644 --- a/docs/en/interfaces/http.md +++ b/docs/en/interfaces/http.md @@ -791,7 +791,42 @@ $ curl -vv -H 'XXX:xxx' 'http://localhost:8123/get_relative_path_static_handler' * Connection #0 to host localhost left intact ``` -## Valid JSON/XML response on exception during HTTP streaming {valid-output-on-exception-http-streaming} +## HTTP Response Headers {#http-response-headers} + +ClickHouse allows you to configure custom HTTP response headers that can be applied to any kind of handler that can be configured. These headers can be set using the `http_response_headers` setting, which accepts key-value pairs representing header names and their values. This feature is particularly useful for implementing custom security headers, CORS policies, or any other HTTP header requirements across your ClickHouse HTTP interface. + +For example, you can configure headers for: +- Regular query endpoints +- Web UI +- Health check. + +It is also possible to specify `common_http_response_headers`. These will be applied to all http handlers defined in the configuration. + +The headers will be included in the HTTP response for every configured handler. + +In the example below, every server response will contain two custom headers: `X-My-Common-Header` and `X-My-Custom-Header`. + +```xml + + + + Common header + + + GET + /ping + + ping + + Custom indeed + + + + + +``` + +## Valid JSON/XML response on exception during HTTP streaming {#valid-output-on-exception-http-streaming} While query execution over HTTP an exception can happen when part of the data has already been sent. Usually an exception is sent to the client in plain text even if some specific data format was used to output data and the output may become invalid in terms of specified data format. diff --git a/src/Server/HTTPHandler.cpp b/src/Server/HTTPHandler.cpp index d2bc22e98cc6..8d8174cb172c 100644 --- a/src/Server/HTTPHandler.cpp +++ b/src/Server/HTTPHandler.cpp @@ -895,11 +895,14 @@ std::string PredefinedQueryHandler::getQuery(HTTPServerRequest & request, HTMLFo HTTPRequestHandlerFactoryPtr createDynamicHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, - const std::string & config_prefix) + const std::string & config_prefix, + std::unordered_map & common_headers) { auto query_param_name = config.getString(config_prefix + ".handler.query_param_name", "query"); HTTPResponseHeaderSetup http_response_headers_override = parseHTTPResponseHeaders(config, config_prefix); + if (http_response_headers_override.has_value()) + http_response_headers_override.value().insert(common_headers.begin(), common_headers.end()); auto creator = [&server, query_param_name, http_response_headers_override]() -> std::unique_ptr { return std::make_unique(server, query_param_name, http_response_headers_override); }; @@ -932,7 +935,8 @@ static inline CompiledRegexPtr getCompiledRegex(const std::string & expression) HTTPRequestHandlerFactoryPtr createPredefinedHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, - const std::string & config_prefix) + const std::string & config_prefix, + std::unordered_map & common_headers) { if (!config.has(config_prefix + ".handler.query")) throw Exception(ErrorCodes::NO_ELEMENTS_IN_CONFIG, "There is no path '{}.handler.query' in configuration file.", config_prefix); @@ -958,6 +962,8 @@ HTTPRequestHandlerFactoryPtr createPredefinedHandlerFactory(IServer & server, } HTTPResponseHeaderSetup http_response_headers_override = parseHTTPResponseHeaders(config, config_prefix); + if (http_response_headers_override.has_value()) + http_response_headers_override.value().insert(common_headers.begin(), common_headers.end()); std::shared_ptr> factory; diff --git a/src/Server/HTTPHandlerFactory.cpp b/src/Server/HTTPHandlerFactory.cpp index fc31ad2874ef..2aa2a56696ba 100644 --- a/src/Server/HTTPHandlerFactory.cpp +++ b/src/Server/HTTPHandlerFactory.cpp @@ -13,6 +13,7 @@ #include "InterserverIOHTTPHandler.h" #include "WebUIRequestHandler.h" +#include namespace DB { @@ -31,27 +32,35 @@ class RedirectRequestHandler : public HTTPRequestHandler { private: std::string url; + std::unordered_map http_response_headers_override; public: - explicit RedirectRequestHandler(std::string url_) - : url(std::move(url_)) + explicit RedirectRequestHandler(std::string url_, std::unordered_map http_response_headers_override_ = {}) + : url(std::move(url_)), http_response_headers_override(http_response_headers_override_) { } void handleRequest(HTTPServerRequest &, HTTPServerResponse & response, const ProfileEvents::Event &) override { + applyHTTPResponseHeaders(response, http_response_headers_override); response.redirect(url); } }; HTTPRequestHandlerFactoryPtr createRedirectHandlerFactory( const Poco::Util::AbstractConfiguration & config, - const std::string & config_prefix) + const std::string & config_prefix, + std::unordered_map common_headers) { std::string url = config.getString(config_prefix + ".handler.location"); + auto headers = parseHTTPResponseHeadersWithCommons(config, config_prefix, common_headers); + auto factory = std::make_shared>( - [my_url = std::move(url)]() { return std::make_unique(my_url); }); + [my_url = std::move(url), headers_override = std::move(headers)]() + { + return std::make_unique(my_url, headers_override); + }); factory->addFiltersFromConfig(config, config_prefix); return factory; @@ -78,6 +87,33 @@ static auto createPingHandlerFactory(IServer & server) return std::make_shared>(std::move(creator)); } +static auto createPingHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, const String & config_prefix, + std::unordered_map common_headers) +{ + auto creator = [&server,&config,config_prefix,common_headers]() -> std::unique_ptr + { + constexpr auto ping_response_expression = "Ok.\n"; + + auto headers = parseHTTPResponseHeadersWithCommons(config, config_prefix, "text/html; charset=UTF-8", common_headers); + + return std::make_unique( + server, ping_response_expression, headers); + }; + return std::make_shared>(std::move(creator)); +} + +template +static auto createWebUIHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, const String & config_prefix, + std::unordered_map common_headers) +{ + auto creator = [&server,&config,config_prefix,common_headers]() -> std::unique_ptr + { + auto headers = parseHTTPResponseHeadersWithCommons(config, config_prefix, "text/html; charset=UTF-8", common_headers); + return std::make_unique(server, headers); + }; + return std::make_shared>(std::move(creator)); +} + static inline auto createHandlersFactoryFromConfig( IServer & server, const Poco::Util::AbstractConfiguration & config, @@ -90,6 +126,19 @@ static inline auto createHandlersFactoryFromConfig( Poco::Util::AbstractConfiguration::Keys keys; config.keys(prefix, keys); + std::unordered_map common_headers_override; + + if (std::find(keys.begin(), keys.end(), "common_http_response_headers") != keys.end()) + { + auto common_headers_prefix = prefix + ".common_http_response_headers"; + Poco::Util::AbstractConfiguration::Keys headers_keys; + config.keys(common_headers_prefix, headers_keys); + for (const auto & header_key : headers_keys) + { + common_headers_override[header_key] = config.getString(common_headers_prefix + "." + header_key); + } + } + for (const auto & key : keys) { if (key == "defaults") @@ -106,50 +155,73 @@ static inline auto createHandlersFactoryFromConfig( if (handler_type == "static") { - main_handler_factory->addHandler(createStaticHandlerFactory(server, config, prefix + "." + key)); + main_handler_factory->addHandler(createStaticHandlerFactory(server, config, prefix + "." + key, common_headers_override)); } else if (handler_type == "redirect") { - main_handler_factory->addHandler(createRedirectHandlerFactory(config, prefix + "." + key)); + main_handler_factory->addHandler(createRedirectHandlerFactory(config, prefix + "." + key, common_headers_override)); } else if (handler_type == "dynamic_query_handler") { - main_handler_factory->addHandler(createDynamicHandlerFactory(server, config, prefix + "." + key)); + main_handler_factory->addHandler(createDynamicHandlerFactory(server, config, prefix + "." + key, common_headers_override)); } else if (handler_type == "predefined_query_handler") { - main_handler_factory->addHandler(createPredefinedHandlerFactory(server, config, prefix + "." + key)); + main_handler_factory->addHandler(createPredefinedHandlerFactory(server, config, prefix + "." + key, common_headers_override)); } else if (handler_type == "prometheus") { main_handler_factory->addHandler( - createPrometheusHandlerFactoryForHTTPRule(server, config, prefix + "." + key, async_metrics)); + createPrometheusHandlerFactoryForHTTPRule(server, config, prefix + "." + key, async_metrics, common_headers_override)); } else if (handler_type == "replicas_status") { - main_handler_factory->addHandler(createReplicasStatusHandlerFactory(server, config, prefix + "." + key)); + main_handler_factory->addHandler(createReplicasStatusHandlerFactory(server, config, prefix + "." + key, common_headers_override)); } else if (handler_type == "ping") { - auto handler = createPingHandlerFactory(server); - handler->addFiltersFromConfig(config, prefix + "." + key); + const String config_prefix = prefix + "." + key; + auto handler = createPingHandlerFactory(server, config, config_prefix, common_headers_override); + handler->addFiltersFromConfig(config, config_prefix); main_handler_factory->addHandler(std::move(handler)); } else if (handler_type == "play") { - auto handler = std::make_shared>(server); + auto handler = createWebUIHandlerFactory(server, config, prefix + "." + key, common_headers_override); handler->addFiltersFromConfig(config, prefix + "." + key); main_handler_factory->addHandler(std::move(handler)); } else if (handler_type == "dashboard") { - auto handler = std::make_shared>(server); + auto handler = createWebUIHandlerFactory(server, config, prefix + "." + key, common_headers_override); handler->addFiltersFromConfig(config, prefix + "." + key); main_handler_factory->addHandler(std::move(handler)); } else if (handler_type == "binary") { - auto handler = std::make_shared>(server); + auto handler = createWebUIHandlerFactory(server, config, prefix + "." + key, common_headers_override); + handler->addFiltersFromConfig(config, prefix + "." + key); + main_handler_factory->addHandler(std::move(handler)); + } + else if (handler_type == "merges") + { + auto handler = createWebUIHandlerFactory(server, config, prefix + "." + key, common_headers_override); + handler->addFiltersFromConfig(config, prefix + "." + key); + main_handler_factory->addHandler(std::move(handler)); + } + else if (handler_type == "js") + { + // NOTE: JavaScriptWebUIRequestHandler only makes sense for paths other then /js/uplot.js, /js/lz-string.js + // because these paths are hardcoded in dashboard.html + const auto & path = config.getString(prefix + "." + key + ".url", ""); + if (path != "/js/uplot.js" && path != "/js/lz-string.js") + { + throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER, + "Handler type 'js' is only supported for url '/js/'. " + "Configured path here: {}", path); + } + + auto handler = createWebUIHandlerFactory(server, config, prefix + "." + key, common_headers_override); handler->addFiltersFromConfig(config, prefix + "." + key); main_handler_factory->addHandler(std::move(handler)); } @@ -157,7 +229,7 @@ static inline auto createHandlersFactoryFromConfig( throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER, "Unknown handler type '{}' in config here: {}.{}.handler.type", handler_type, prefix, key); } - else + else if (key != "common_http_response_headers") throw Exception(ErrorCodes::UNKNOWN_ELEMENT_IN_CONFIG, "Unknown element in config: " "{}.{}, must be 'rule' or 'defaults'", prefix, key); } diff --git a/src/Server/HTTPHandlerFactory.h b/src/Server/HTTPHandlerFactory.h index db4bb73cbc44..f2b7760f8b02 100644 --- a/src/Server/HTTPHandlerFactory.h +++ b/src/Server/HTTPHandlerFactory.h @@ -110,19 +110,23 @@ class HandlingRuleHTTPHandlerFactory : public HTTPRequestHandlerFactory HTTPRequestHandlerFactoryPtr createStaticHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, - const std::string & config_prefix); + const std::string & config_prefix, + std::unordered_map & common_headers); HTTPRequestHandlerFactoryPtr createDynamicHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, - const std::string & config_prefix); + const std::string & config_prefix, + std::unordered_map & common_headers); HTTPRequestHandlerFactoryPtr createPredefinedHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, - const std::string & config_prefix); + const std::string & config_prefix, + std::unordered_map & common_headers); HTTPRequestHandlerFactoryPtr createReplicasStatusHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, - const std::string & config_prefix); + const std::string & config_prefix, + std::unordered_map & common_headers); /// @param server - used in handlers to check IServer::isCancelled() /// @param config - not the same as server.config(), since it can be newer diff --git a/src/Server/HTTPResponseHeaderWriter.cpp b/src/Server/HTTPResponseHeaderWriter.cpp index fd29af5bdc77..912b696faffb 100644 --- a/src/Server/HTTPResponseHeaderWriter.cpp +++ b/src/Server/HTTPResponseHeaderWriter.cpp @@ -66,4 +66,25 @@ void applyHTTPResponseHeaders(Poco::Net::HTTPResponse & response, const std::uno response.set(header_name, header_value); } +std::unordered_map parseHTTPResponseHeadersWithCommons( + const Poco::Util::AbstractConfiguration & config, + const std::string & config_prefix, + const std::string & default_content_type, + const std::unordered_map & common_headers) +{ + auto headers = parseHTTPResponseHeaders(config, config_prefix, default_content_type); + headers.insert(common_headers.begin(), common_headers.end()); + return headers; +} + +std::unordered_map parseHTTPResponseHeadersWithCommons( + const Poco::Util::AbstractConfiguration & config, + const std::string & config_prefix, + const std::unordered_map & common_headers) +{ + auto headers = parseHTTPResponseHeaders(config, config_prefix).value_or(std::unordered_map{}); + headers.insert(common_headers.begin(), common_headers.end()); + return headers; +} + } diff --git a/src/Server/HTTPResponseHeaderWriter.h b/src/Server/HTTPResponseHeaderWriter.h index 06281abb42dd..7e240b9eddec 100644 --- a/src/Server/HTTPResponseHeaderWriter.h +++ b/src/Server/HTTPResponseHeaderWriter.h @@ -22,4 +22,16 @@ std::unordered_map parseHTTPResponseHeaders(const std::string & void applyHTTPResponseHeaders(Poco::Net::HTTPResponse & response, const HTTPResponseHeaderSetup & setup); void applyHTTPResponseHeaders(Poco::Net::HTTPResponse & response, const std::unordered_map & setup); + +std::unordered_map parseHTTPResponseHeadersWithCommons( + const Poco::Util::AbstractConfiguration & config, + const std::string & config_prefix, + const std::unordered_map & common_headers); + +std::unordered_map parseHTTPResponseHeadersWithCommons( + const Poco::Util::AbstractConfiguration & config, + const std::string & config_prefix, + const std::string & default_content_type, + const std::unordered_map & common_headers); + } diff --git a/src/Server/PrometheusRequestHandler.cpp b/src/Server/PrometheusRequestHandler.cpp index ae1fb6d629e0..e9bfb9fe5789 100644 --- a/src/Server/PrometheusRequestHandler.cpp +++ b/src/Server/PrometheusRequestHandler.cpp @@ -303,13 +303,15 @@ PrometheusRequestHandler::PrometheusRequestHandler( IServer & server_, const PrometheusRequestHandlerConfig & config_, const AsynchronousMetrics & async_metrics_, - std::shared_ptr metrics_writer_) + std::shared_ptr metrics_writer_, + std::unordered_map response_headers_) : server(server_) , config(config_) , async_metrics(async_metrics_) , metrics_writer(metrics_writer_) , log(getLogger("PrometheusRequestHandler")) { + response_headers = response_headers_; createImpl(); } @@ -341,6 +343,7 @@ void PrometheusRequestHandler::createImpl() void PrometheusRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event_) { setThreadName("PrometheusHndlr"); + applyHTTPResponseHeaders(response, response_headers); try { diff --git a/src/Server/PrometheusRequestHandler.h b/src/Server/PrometheusRequestHandler.h index 281ecf5260ed..550404f7cf64 100644 --- a/src/Server/PrometheusRequestHandler.h +++ b/src/Server/PrometheusRequestHandler.h @@ -19,7 +19,8 @@ class PrometheusRequestHandler : public HTTPRequestHandler IServer & server_, const PrometheusRequestHandlerConfig & config_, const AsynchronousMetrics & async_metrics_, - std::shared_ptr metrics_writer_); + std::shared_ptr metrics_writer_, + std::unordered_map response_headers_ = {}); ~PrometheusRequestHandler() override; void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event_) override; @@ -59,6 +60,7 @@ class PrometheusRequestHandler : public HTTPRequestHandler std::unique_ptr write_buffer_from_response; bool response_finalized = false; ProfileEvents::Event write_event; + std::unordered_map response_headers; }; } diff --git a/src/Server/PrometheusRequestHandlerFactory.cpp b/src/Server/PrometheusRequestHandlerFactory.cpp index 52f1d3b64c14..72811d88e67d 100644 --- a/src/Server/PrometheusRequestHandlerFactory.cpp +++ b/src/Server/PrometheusRequestHandlerFactory.cpp @@ -131,14 +131,15 @@ namespace IServer & server, const AsynchronousMetrics & async_metrics, const PrometheusRequestHandlerConfig & config, - bool for_keeper) + bool for_keeper, + std::unordered_map headers = {}) { if (!canBeHandled(config, for_keeper)) return nullptr; auto metric_writer = createPrometheusMetricWriter(for_keeper); - auto creator = [&server, &async_metrics, config, metric_writer]() -> std::unique_ptr + auto creator = [&server, &async_metrics, config, metric_writer, headers_moved = std::move(headers)]() -> std::unique_ptr { - return std::make_unique(server, config, async_metrics, metric_writer); + return std::make_unique(server, config, async_metrics, metric_writer, headers_moved); }; return std::make_shared>(std::move(creator)); } @@ -200,10 +201,13 @@ HTTPRequestHandlerFactoryPtr createPrometheusHandlerFactoryForHTTPRule( IServer & server, const Poco::Util::AbstractConfiguration & config, const String & config_prefix, - const AsynchronousMetrics & asynchronous_metrics) + const AsynchronousMetrics & asynchronous_metrics, + std::unordered_map & common_headers) { + auto headers = parseHTTPResponseHeadersWithCommons(config, config_prefix, common_headers); + auto parsed_config = parseExposeMetricsConfig(config, config_prefix + ".handler"); - auto handler = createPrometheusHandlerFactoryFromConfig(server, asynchronous_metrics, parsed_config, /* for_keeper= */ false); + auto handler = createPrometheusHandlerFactoryFromConfig(server, asynchronous_metrics, parsed_config, /* for_keeper= */ false, headers); chassert(handler); /// `handler` can't be nullptr here because `for_keeper` is false. handler->addFiltersFromConfig(config, config_prefix); return handler; diff --git a/src/Server/PrometheusRequestHandlerFactory.h b/src/Server/PrometheusRequestHandlerFactory.h index c52395ca93f3..23d00b50095b 100644 --- a/src/Server/PrometheusRequestHandlerFactory.h +++ b/src/Server/PrometheusRequestHandlerFactory.h @@ -93,7 +93,8 @@ HTTPRequestHandlerFactoryPtr createPrometheusHandlerFactoryForHTTPRule( IServer & server, const Poco::Util::AbstractConfiguration & config, const String & config_prefix, /// path to "http_handlers.my_handler_1" - const AsynchronousMetrics & asynchronous_metrics); + const AsynchronousMetrics & asynchronous_metrics, + std::unordered_map & common_headers); /// Makes a HTTP Handler factory to handle requests for prometheus metrics as a part of the default HTTP rule in the section. /// Expects a configuration like this: diff --git a/src/Server/ReplicasStatusHandler.cpp b/src/Server/ReplicasStatusHandler.cpp index 419ad635d0d5..042c9657f6d2 100644 --- a/src/Server/ReplicasStatusHandler.cpp +++ b/src/Server/ReplicasStatusHandler.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,8 @@ void ReplicasStatusHandler::handleRequest(HTTPServerRequest & request, HTTPServe { try { + applyHTTPResponseHeaders(response, http_response_headers_override); + HTMLForm params(getContext()->getSettingsRef(), request); const auto & config = getContext()->getConfigRef(); @@ -129,9 +132,16 @@ void ReplicasStatusHandler::handleRequest(HTTPServerRequest & request, HTTPServe HTTPRequestHandlerFactoryPtr createReplicasStatusHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, - const std::string & config_prefix) + const std::string & config_prefix, + std::unordered_map & common_headers) { - auto factory = std::make_shared>(server); + std::unordered_map http_response_headers_override + = parseHTTPResponseHeadersWithCommons(config, config_prefix, "text/plain; charset=UTF-8", common_headers); + + auto creator = [&server, http_response_headers_override]() -> std::unique_ptr + { return std::make_unique(server, http_response_headers_override); }; + + auto factory = std::make_shared>(std::move(creator)); factory->addFiltersFromConfig(config, config_prefix); return factory; } diff --git a/src/Server/ReplicasStatusHandler.h b/src/Server/ReplicasStatusHandler.h index 08fd757b0d61..2d3aaad184b5 100644 --- a/src/Server/ReplicasStatusHandler.h +++ b/src/Server/ReplicasStatusHandler.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace DB { @@ -13,8 +14,17 @@ class ReplicasStatusHandler : public HTTPRequestHandler, WithContext { public: explicit ReplicasStatusHandler(IServer & server_); + explicit ReplicasStatusHandler(IServer & server_, const std::unordered_map & http_response_headers_override_) + : ReplicasStatusHandler(server_) + { + http_response_headers_override = http_response_headers_override_; + } void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override; + +private: + /// Overrides for response headers. + std::unordered_map http_response_headers_override; }; diff --git a/src/Server/StaticRequestHandler.cpp b/src/Server/StaticRequestHandler.cpp index d8c0765bca48..34774d6689d8 100644 --- a/src/Server/StaticRequestHandler.cpp +++ b/src/Server/StaticRequestHandler.cpp @@ -163,13 +163,14 @@ StaticRequestHandler::StaticRequestHandler( HTTPRequestHandlerFactoryPtr createStaticHandlerFactory(IServer & server, const Poco::Util::AbstractConfiguration & config, - const std::string & config_prefix) + const std::string & config_prefix, + std::unordered_map & common_headers) { int status = config.getInt(config_prefix + ".handler.status", 200); std::string response_content = config.getRawString(config_prefix + ".handler.response_content", "Ok.\n"); std::unordered_map http_response_headers_override - = parseHTTPResponseHeaders(config, config_prefix, "text/plain; charset=UTF-8"); + = parseHTTPResponseHeadersWithCommons(config, config_prefix, "text/plain; charset=UTF-8", common_headers); auto creator = [&server, http_response_headers_override, response_content, status]() -> std::unique_ptr { return std::make_unique(server, response_content, http_response_headers_override, status); }; diff --git a/src/Server/WebUIRequestHandler.cpp b/src/Server/WebUIRequestHandler.cpp index c04d7a3f2a03..b382da29c038 100644 --- a/src/Server/WebUIRequestHandler.cpp +++ b/src/Server/WebUIRequestHandler.cpp @@ -1,6 +1,7 @@ #include "WebUIRequestHandler.h" #include "IServer.h" #include +#include #include #include @@ -30,8 +31,10 @@ DashboardWebUIRequestHandler::DashboardWebUIRequestHandler(IServer & server_) : BinaryWebUIRequestHandler::BinaryWebUIRequestHandler(IServer & server_) : server(server_) {} JavaScriptWebUIRequestHandler::JavaScriptWebUIRequestHandler(IServer & server_) : server(server_) {} -static void handle(HTTPServerRequest & request, HTTPServerResponse & response, std::string_view html) +static void handle(HTTPServerRequest & request, HTTPServerResponse & response, std::string_view html, + std::unordered_map http_response_headers_override = {}) { + applyHTTPResponseHeaders(response, http_response_headers_override); response.setContentType("text/html; charset=UTF-8"); if (request.getVersion() == HTTPServerRequest::HTTP_1_1) response.setChunkedTransferEncoding(true); @@ -43,7 +46,7 @@ static void handle(HTTPServerRequest & request, HTTPServerResponse & response, s void PlayWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event &) { - handle(request, response, {reinterpret_cast(gresource_play_htmlData), gresource_play_htmlSize}); + handle(request, response, {reinterpret_cast(gresource_play_htmlData), gresource_play_htmlSize}, http_response_headers_override); } void DashboardWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event &) @@ -61,23 +64,28 @@ void DashboardWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HT static re2::RE2 lz_string_url = R"(https://[^\s"'`]+lz-string[^\s"'`]*\.js)"; RE2::Replace(&html, lz_string_url, "/js/lz-string.js"); - handle(request, response, html); + handle(request, response, html, http_response_headers_override); } void BinaryWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event &) { - handle(request, response, {reinterpret_cast(gresource_binary_htmlData), gresource_binary_htmlSize}); + handle(request, response, {reinterpret_cast(gresource_binary_htmlData), gresource_binary_htmlSize}, http_response_headers_override); +} + +void MergesWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event &) +{ + handle(request, response, {reinterpret_cast(gresource_merges_htmlData), gresource_merges_htmlSize}, http_response_headers_override); } void JavaScriptWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event &) { if (request.getURI() == "/js/uplot.js") { - handle(request, response, {reinterpret_cast(gresource_uplot_jsData), gresource_uplot_jsSize}); + handle(request, response, {reinterpret_cast(gresource_uplot_jsData), gresource_uplot_jsSize}, http_response_headers_override); } else if (request.getURI() == "/js/lz-string.js") { - handle(request, response, {reinterpret_cast(gresource_lz_string_jsData), gresource_lz_string_jsSize}); + handle(request, response, {reinterpret_cast(gresource_lz_string_jsData), gresource_lz_string_jsSize}, http_response_headers_override); } else { @@ -85,7 +93,7 @@ void JavaScriptWebUIRequestHandler::handleRequest(HTTPServerRequest & request, H *response.send() << "Not found.\n"; } - handle(request, response, {reinterpret_cast(gresource_binary_htmlData), gresource_binary_htmlSize}); + handle(request, response, {reinterpret_cast(gresource_binary_htmlData), gresource_binary_htmlSize}, http_response_headers_override); } } diff --git a/src/Server/WebUIRequestHandler.h b/src/Server/WebUIRequestHandler.h index b84c8f6534d7..91b74bae5863 100644 --- a/src/Server/WebUIRequestHandler.h +++ b/src/Server/WebUIRequestHandler.h @@ -16,7 +16,15 @@ class PlayWebUIRequestHandler : public HTTPRequestHandler IServer & server; public: explicit PlayWebUIRequestHandler(IServer & server_); + explicit PlayWebUIRequestHandler(IServer & server_, const std::unordered_map & http_response_headers_override_) + : PlayWebUIRequestHandler(server_) + { + http_response_headers_override = http_response_headers_override_; + } void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override; +private: + /// Overrides for response headers. + std::unordered_map http_response_headers_override; }; class DashboardWebUIRequestHandler : public HTTPRequestHandler @@ -25,7 +33,15 @@ class DashboardWebUIRequestHandler : public HTTPRequestHandler IServer & server; public: explicit DashboardWebUIRequestHandler(IServer & server_); + explicit DashboardWebUIRequestHandler(IServer & server_, const std::unordered_map & http_response_headers_override_) + : DashboardWebUIRequestHandler(server_) + { + http_response_headers_override = http_response_headers_override_; + } void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override; +private: + /// Overrides for response headers. + std::unordered_map http_response_headers_override; }; class BinaryWebUIRequestHandler : public HTTPRequestHandler @@ -34,7 +50,30 @@ class BinaryWebUIRequestHandler : public HTTPRequestHandler IServer & server; public: explicit BinaryWebUIRequestHandler(IServer & server_); + explicit BinaryWebUIRequestHandler(IServer & server_, const std::unordered_map & http_response_headers_override_) + : BinaryWebUIRequestHandler(server_) + { + http_response_headers_override = http_response_headers_override_; + } + void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override; +private: + /// Overrides for response headers. + std::unordered_map http_response_headers_override; +}; + +class MergesWebUIRequestHandler : public HTTPRequestHandler +{ +public: + explicit MergesWebUIRequestHandler(IServer &) {} + explicit MergesWebUIRequestHandler(IServer & server_, const std::unordered_map & http_response_headers_override_) + : MergesWebUIRequestHandler(server_) + { + http_response_headers_override = http_response_headers_override_; + } void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override; +private: + /// Overrides for response headers. + std::unordered_map http_response_headers_override; }; class JavaScriptWebUIRequestHandler : public HTTPRequestHandler @@ -43,7 +82,15 @@ class JavaScriptWebUIRequestHandler : public HTTPRequestHandler IServer & server; public: explicit JavaScriptWebUIRequestHandler(IServer & server_); + explicit JavaScriptWebUIRequestHandler(IServer & server_, const std::unordered_map & http_response_headers_override_) + : JavaScriptWebUIRequestHandler(server_) + { + http_response_headers_override = http_response_headers_override_; + } void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override; +private: + /// Overrides for response headers. + std::unordered_map http_response_headers_override; }; } diff --git a/tests/integration/test_http_handlers_config/test.py b/tests/integration/test_http_handlers_config/test.py index b2efbf4bb657..9d2230b37cd6 100644 --- a/tests/integration/test_http_handlers_config/test.py +++ b/tests/integration/test_http_handlers_config/test.py @@ -601,3 +601,31 @@ def test_replicas_status_handler(): "test_replicas_status", method="GET", headers={"XXX": "xxx"} ).content ) + + +def test_headers_in_response(): + with contextlib.closing( + SimpleCluster( + ClickHouseCluster(__file__), "headers_in_response", "test_headers_in_response" + ) + ) as cluster: + for endpoint in ("static", "ping", "replicas_status", "play", "dashboard", "binary", "merges", "metrics", + "js/lz-string.js", "js/uplot.js", "?query=SELECT%201"): + response = cluster.instance.http_request(endpoint, method="GET") + + assert "X-My-Answer" in response.headers + assert "X-My-Common-Header" in response.headers + + assert response.headers["X-My-Common-Header"] == "Common header present" + + if endpoint == "?query=SELECT%201": + assert response.headers["X-My-Answer"] == "Iam dynamic" + else: + assert response.headers["X-My-Answer"] == f"Iam {endpoint}" + + + # Handle predefined_query_handler separately because we need to pass headers there + response_predefined = cluster.instance.http_request( + "query_param_with_url", method="GET", headers={"PARAMS_XXX": "test_param"}) + assert response_predefined.headers["X-My-Answer"] == f"Iam predefined" + assert response_predefined.headers["X-My-Common-Header"] == "Common header present" diff --git a/tests/integration/test_http_handlers_config/test_headers_in_response/config.xml b/tests/integration/test_http_handlers_config/test_headers_in_response/config.xml new file mode 100644 index 000000000000..6fc72a1af5f5 --- /dev/null +++ b/tests/integration/test_http_handlers_config/test_headers_in_response/config.xml @@ -0,0 +1,146 @@ + + + + Common header present + + + + GET,HEAD + /static + + static + config://http_server_default_response + text/html; charset=UTF-8 + + Iam static + + + + + + GET,HEAD + /ping + + ping + + Iam ping + + + + + + GET,HEAD + /replicas_status + + replicas_status + + Iam replicas_status + + + + + + GET,HEAD + /play + + play + + Iam play + + + + + + GET,HEAD + /dashboard + + dashboard + + Iam dashboard + + + + + + GET,HEAD + /binary + + binary + + Iam binary + + + + + + GET,HEAD + /merges + + merges + + Iam merges + + + + + + GET,HEAD + /metrics + + prometheus + + Iam metrics + + + + + + /js/lz-string.js + + js + + Iam js/lz-string.js + + + + + + GET,HEAD + /js/uplot.js + + js + + Iam js/uplot.js + + + + + + /query_param_with_url + GET,HEAD + + [^/]+)]]> + + + predefined_query_handler + + SELECT {name_1:String} + + + Iam predefined + + + + + + GET,POST,HEAD,OPTIONS + + dynamic_query_handler + query + + Iam dynamic + + + + + \ No newline at end of file From 6c3166412be9b607e7177c685d08d0ab313a7de8 Mon Sep 17 00:00:00 2001 From: Andrey Zvonov Date: Tue, 3 Jun 2025 14:45:30 +0300 Subject: [PATCH 2/5] fix build --- src/Server/PrometheusRequestHandlerFactory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Server/PrometheusRequestHandlerFactory.cpp b/src/Server/PrometheusRequestHandlerFactory.cpp index 72811d88e67d..65061a5cc018 100644 --- a/src/Server/PrometheusRequestHandlerFactory.cpp +++ b/src/Server/PrometheusRequestHandlerFactory.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include From 698ef79bcf991b1f113c75532f7298728965fb48 Mon Sep 17 00:00:00 2001 From: Andrey Zvonov Date: Tue, 3 Jun 2025 15:40:17 +0300 Subject: [PATCH 3/5] remove pieces of code from future --- src/Server/HTTPHandlerFactory.cpp | 6 ------ src/Server/WebUIRequestHandler.cpp | 5 ----- src/Server/WebUIRequestHandler.h | 15 --------------- 3 files changed, 26 deletions(-) diff --git a/src/Server/HTTPHandlerFactory.cpp b/src/Server/HTTPHandlerFactory.cpp index 2aa2a56696ba..ff3c5e2d4d60 100644 --- a/src/Server/HTTPHandlerFactory.cpp +++ b/src/Server/HTTPHandlerFactory.cpp @@ -203,12 +203,6 @@ static inline auto createHandlersFactoryFromConfig( handler->addFiltersFromConfig(config, prefix + "." + key); main_handler_factory->addHandler(std::move(handler)); } - else if (handler_type == "merges") - { - auto handler = createWebUIHandlerFactory(server, config, prefix + "." + key, common_headers_override); - handler->addFiltersFromConfig(config, prefix + "." + key); - main_handler_factory->addHandler(std::move(handler)); - } else if (handler_type == "js") { // NOTE: JavaScriptWebUIRequestHandler only makes sense for paths other then /js/uplot.js, /js/lz-string.js diff --git a/src/Server/WebUIRequestHandler.cpp b/src/Server/WebUIRequestHandler.cpp index b382da29c038..7f1a06b27858 100644 --- a/src/Server/WebUIRequestHandler.cpp +++ b/src/Server/WebUIRequestHandler.cpp @@ -72,11 +72,6 @@ void BinaryWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPS handle(request, response, {reinterpret_cast(gresource_binary_htmlData), gresource_binary_htmlSize}, http_response_headers_override); } -void MergesWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event &) -{ - handle(request, response, {reinterpret_cast(gresource_merges_htmlData), gresource_merges_htmlSize}, http_response_headers_override); -} - void JavaScriptWebUIRequestHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event &) { if (request.getURI() == "/js/uplot.js") diff --git a/src/Server/WebUIRequestHandler.h b/src/Server/WebUIRequestHandler.h index 91b74bae5863..8fa2f10daae0 100644 --- a/src/Server/WebUIRequestHandler.h +++ b/src/Server/WebUIRequestHandler.h @@ -61,21 +61,6 @@ class BinaryWebUIRequestHandler : public HTTPRequestHandler std::unordered_map http_response_headers_override; }; -class MergesWebUIRequestHandler : public HTTPRequestHandler -{ -public: - explicit MergesWebUIRequestHandler(IServer &) {} - explicit MergesWebUIRequestHandler(IServer & server_, const std::unordered_map & http_response_headers_override_) - : MergesWebUIRequestHandler(server_) - { - http_response_headers_override = http_response_headers_override_; - } - void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override; -private: - /// Overrides for response headers. - std::unordered_map http_response_headers_override; -}; - class JavaScriptWebUIRequestHandler : public HTTPRequestHandler { private: From 429e5c8013d71a1d0e86b4bf31117bcaa7f5ccba Mon Sep 17 00:00:00 2001 From: Andrey Zvonov Date: Tue, 3 Jun 2025 16:40:47 +0300 Subject: [PATCH 4/5] fix build --- src/Server/PrometheusRequestHandler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Server/PrometheusRequestHandler.cpp b/src/Server/PrometheusRequestHandler.cpp index e9bfb9fe5789..d99b2c825598 100644 --- a/src/Server/PrometheusRequestHandler.cpp +++ b/src/Server/PrometheusRequestHandler.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include From 81c082fb9db07cba7675efab941bc0f11adebb1f Mon Sep 17 00:00:00 2001 From: Andrey Zvonov Date: Wed, 4 Jun 2025 10:37:05 +0300 Subject: [PATCH 5/5] remove 'merges' from test as it only appears in later versions --- tests/integration/test_http_handlers_config/test.py | 2 +- .../test_headers_in_response/config.xml | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/integration/test_http_handlers_config/test.py b/tests/integration/test_http_handlers_config/test.py index 9d2230b37cd6..091690cf6b31 100644 --- a/tests/integration/test_http_handlers_config/test.py +++ b/tests/integration/test_http_handlers_config/test.py @@ -609,7 +609,7 @@ def test_headers_in_response(): ClickHouseCluster(__file__), "headers_in_response", "test_headers_in_response" ) ) as cluster: - for endpoint in ("static", "ping", "replicas_status", "play", "dashboard", "binary", "merges", "metrics", + for endpoint in ("static", "ping", "replicas_status", "play", "dashboard", "binary", "metrics", "js/lz-string.js", "js/uplot.js", "?query=SELECT%201"): response = cluster.instance.http_request(endpoint, method="GET") diff --git a/tests/integration/test_http_handlers_config/test_headers_in_response/config.xml b/tests/integration/test_http_handlers_config/test_headers_in_response/config.xml index 6fc72a1af5f5..4ee24ae123f8 100644 --- a/tests/integration/test_http_handlers_config/test_headers_in_response/config.xml +++ b/tests/integration/test_http_handlers_config/test_headers_in_response/config.xml @@ -72,17 +72,6 @@ - - GET,HEAD - /merges - - merges - - Iam merges - - - - GET,HEAD /metrics