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..ff3c5e2d4d60 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,67 @@ 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 == "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 +223,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..d99b2c825598 100644
--- a/src/Server/PrometheusRequestHandler.cpp
+++ b/src/Server/PrometheusRequestHandler.cpp
@@ -3,6 +3,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -303,13 +304,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 +344,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..65061a5cc018 100644
--- a/src/Server/PrometheusRequestHandlerFactory.cpp
+++ b/src/Server/PrometheusRequestHandlerFactory.cpp
@@ -1,6 +1,7 @@
#include
#include
+#include
#include
#include
#include
@@ -131,14 +132,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 +202,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..7f1a06b27858 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,23 @@ 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 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 +88,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..8fa2f10daae0 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,15 @@ 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 JavaScriptWebUIRequestHandler : public HTTPRequestHandler
@@ -43,7 +67,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..091690cf6b31 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", "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..4ee24ae123f8
--- /dev/null
+++ b/tests/integration/test_http_handlers_config/test_headers_in_response/config.xml
@@ -0,0 +1,135 @@
+
+
+
+ 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
+ /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