Add http_response_headers setting

This commit is contained in:
Alexey Milovidov 2024-11-30 22:02:26 +01:00
parent dc4bbc8d73
commit 076a8f8d9e
6 changed files with 84 additions and 1 deletions

View File

@ -1816,6 +1816,22 @@ Possible values:
- 0 Disabled.
- 1 Enabled.
)", 0) \
DECLARE(Map, http_response_headers, "", R"(
Allows to add or override HTTP headers which the server will return it the response with a successful query result.
This only affects the HTTP interface.
If the header is already set by default, the provided value will override it.
If the header was not set by default, it will be added to the list of headers.
Headers that are set by the server by default and not overridden by this setting, will remain.
The setting allows you to set a header to a constant value. Currently there is no way to set a header to a dynamically calculated value.
Neither names or values can contain ASCII control characters.
If you implement a UI application which allows users to modify settings but at the same time makes decisions based on the returned headers, it is recommended to restrict this setting to readonly.
Example: `SET http_response_headers = '{"Content-Type": "image/png"}'`
)", 0) \
\
DECLARE(String, count_distinct_implementation, "uniqExact", R"(

View File

@ -163,6 +163,7 @@ namespace Setting
extern const SettingsSeconds wait_for_async_insert_timeout;
extern const SettingsBool implicit_select;
extern const SettingsBool enforce_strict_identifier_format;
extern const SettingsMap http_response_headers;
}
namespace ErrorCodes
@ -1682,6 +1683,32 @@ void executeQuery(
/// But `session_timezone` setting could be modified in the query itself, so we update the value.
result_details.timezone = DateLUT::instance().getTimeZone();
const Map & additional_http_headers = context->getSettingsRef()[Setting::http_response_headers].value;
if (!additional_http_headers.empty())
{
for (const auto & key_value : additional_http_headers)
{
if (key_value.getType() != Field::Types::Tuple
|| key_value.safeGet<Tuple>().size() != 2)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "The value of the `additional_http_headers` setting must be a Map");
if (key_value.safeGet<Tuple>().at(0).getType() != Field::Types::String)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "The keys of the `additional_http_headers` setting must be Strings");
if (key_value.safeGet<Tuple>().at(1).getType() != Field::Types::String)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "The values of the `additional_http_headers` setting must be Strings");
String key = key_value.safeGet<Tuple>().at(0).safeGet<String>();
String value = key_value.safeGet<Tuple>().at(1).safeGet<String>();
if (std::find_if(key.begin(), key.end(), isControlASCII) != key.end()
|| std::find_if(value.begin(), value.end(), isControlASCII) != value.end())
throw Exception(ErrorCodes::BAD_ARGUMENTS, "The values of the `additional_http_headers` cannot contain ASCII control characters");
result_details.additional_headers.emplace(key, value);
}
}
auto & pipeline = streams.pipeline;
std::unique_ptr<WriteBuffer> compressed_buffer;

View File

@ -24,6 +24,7 @@ struct QueryResultDetails
std::optional<String> content_type = {};
std::optional<String> format = {};
std::optional<String> timezone = {};
std::unordered_map<String, String> additional_headers = {};
};
using SetResultDetailsFunc = std::function<void(const QueryResultDetails &)>;
@ -42,7 +43,7 @@ void executeQuery(
WriteBuffer & ostr, /// Where to write query output to.
bool allow_into_outfile, /// If true and the query contains INTO OUTFILE section, redirect output to that file.
ContextMutablePtr context, /// DB, tables, data types, storage engines, functions, aggregate functions...
SetResultDetailsFunc set_result_details, /// If a non-empty callback is passed, it will be called with the query id, the content-type, the format, and the timezone.
SetResultDetailsFunc set_result_details, /// If a non-empty callback is passed, it will be called with the query id, the content-type, the format, and the timezone, as well as additional headers.
QueryFlags flags = {},
const std::optional<FormatSettings> & output_format_settings = std::nullopt, /// Format settings for output format, will be calculated from the context if not set.
HandleExceptionInOutputFormatFunc handle_exception_in_output_format = {} /// If a non-empty callback is passed, it will be called on exception with created output format.

View File

@ -524,6 +524,9 @@ void HTTPHandler::processQuery(
if (details.timezone)
response.add("X-ClickHouse-Timezone", *details.timezone);
for (const auto & [name, value] : details.additional_headers)
response.set(name, value);
};
auto handle_exception_in_output_format = [&](IOutputFormat & current_output_format,

View File

@ -0,0 +1,15 @@
We can add a new header:
> POST /?http_response_headers={'My-New-Header':'Hello,+world.'} HTTP/1.1
< My-New-Header: Hello, world.
It works even with the settings clause:
< My-New-Header: Hello, world.
Check the default header value:
> Content-Type: application/x-www-form-urlencoded
< Content-Type: text/tab-separated-values; charset=UTF-8
Check that we can override it:
> POST /?http_response_headers={'Content-Type':'image/png'} HTTP/1.1
> Content-Type: application/x-www-form-urlencoded
< Content-Type: image/png
It does not allow bad characters:
BAD_ARGUMENTS
BAD_ARGUMENTS

View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
CUR_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=../shell_config.sh
. "$CUR_DIR"/../shell_config.sh
echo "We can add a new header:"
${CLICKHOUSE_CURL} -sS --globoff -v "http://localhost:8123/?http_response_headers={'My-New-Header':'Hello,+world.'}" -d "SELECT 1" 2>&1 | grep -i 'My-New'
echo "It works even with the settings clause:"
${CLICKHOUSE_CURL} -sS --globoff -v "http://localhost:8123/" -d "SELECT 1 SETTINGS http_response_headers = \$\${'My-New-Header':'Hello, world.'}\$\$" 2>&1 | grep -i 'My-New'
echo "Check the default header value:"
${CLICKHOUSE_CURL} -sS --globoff -v "http://localhost:8123/" -d "SELECT 1" 2>&1 | grep -i 'Content-Type'
echo "Check that we can override it:"
${CLICKHOUSE_CURL} -sS --globoff -v "http://localhost:8123/?http_response_headers={'Content-Type':'image/png'}" -d "SELECT 1" 2>&1 | grep -i 'Content-Type'
echo "It does not allow bad characters:"
${CLICKHOUSE_CURL} -sS --globoff -v "http://localhost:8123/" -d "SELECT 1 SETTINGS http_response_headers = \$\${'My-New-Header':'Hello,\n\nworld.'}\$\$" 2>&1 | grep -o -F 'BAD_ARGUMENTS'
${CLICKHOUSE_CURL} -sS --globoff -v "http://localhost:8123/" -d "SELECT 1 SETTINGS http_response_headers = \$\${'My\rNew-Header':'Hello, world.'}\$\$" 2>&1 | grep -o -F 'BAD_ARGUMENTS'