Describe the bug
Setting any of .NET's known content headers via the public CustomHeaders option — either
per-query (QueryOptions.CustomHeaders) or client-level (ClickHouseClientSettings.CustomHeaders) —
makes every request throw before it is sent:
System.InvalidOperationException: Misused header name, 'Content-Type'. Make sure request headers are
used with HttpRequestMessage, response headers with HttpResponseMessage, and content headers with
HttpContent objects.
The XML docs on both properties say custom headers "are applied after the default headers, allowing
you to override most headers" and that only Connection, Authorization and User-Agent "cannot be
overridden and will be silently ignored". A whole class of header names instead escapes as an
unhandled framework exception, which is neither documented nor caught.
All 11 headers that .NET classifies as content headers reproduce it: Allow, Content-Disposition,
Content-Encoding, Content-Language, Content-Length, Content-Location, Content-MD5,
Content-Range, Content-Type, Expires, Last-Modified. Non-content headers are unaffected
(X-*, Accept-Encoding, Cache-Control, Referer all work).
It affects every entry point that goes through AddDefaultHttpHeaders — verified on
ExecuteScalarAsync, ExecuteNonQueryAsync, ExecuteReaderAsync and ExecuteRawResultAsync,
at both the client and per-query level.
Steps to reproduce
- Create a client (or
QueryOptions) with a content header in CustomHeaders.
- Run any query.
- The call throws
InvalidOperationException before any request reaches the server.
Expected behaviour
One of the documented outcomes, not an unhandled framework exception:
- the header is applied, or
- the header is silently ignored like the other non-overridable headers (and the docs list it), or
- the option is rejected up front with a clear
ArgumentException naming the header and why.
Code example
// per-query
using var client = new ClickHouseClient("Host=localhost");
var options = new QueryOptions
{
CustomHeaders = new Dictionary<string, string> { ["Content-Type"] = "text/plain" },
};
await client.ExecuteScalarAsync("SELECT 1", options: options); // throws
// client-level — same result
var settings = new ClickHouseClientSettings("Host=localhost")
{
CustomHeaders = new Dictionary<string, string> { ["Content-Encoding"] = "gzip" },
};
using var client2 = new ClickHouseClient(settings);
await client2.ExecuteScalarAsync("SELECT 1"); // throws
Error log
System.InvalidOperationException: Misused header name, 'Content-Encoding'. Make sure request headers
are used with HttpRequestMessage, response headers with HttpResponseMessage, and content headers with
HttpContent objects.
Root cause
ClickHouseClient.ApplyCustomHeaders (ClickHouse.Driver/ClickHouseClient.cs:1124-1137) applies every
non-blocked custom header to the request's HttpRequestHeaders:
if (!IsBlockedHeader(kvp.Key))
{
requestHeaders.Remove(kvp.Key); // <-- throws here
requestHeaders.TryAddWithoutValidation(kvp.Key, kvp.Value);
}
The throwing call is the Remove, not the add. Probed directly on .NET 10:
HttpRequestHeaders.Remove("Content-Type") → throws InvalidOperationException (Contains throws too)
HttpRequestHeaders.TryAddWithoutValidation("Content-Type", …) → returns false, no throw
So TryAddWithoutValidation already degrades gracefully; the Remove that was added to give custom
headers "last write wins" semantics is what turns a no-op into an exception.
IsBlockedHeader (ClickHouseClient.cs:1334-1339) only covers Connection, Authorization,
User-Agent.
Suggested fix
Smallest change that matches the documented contract: make the removal tolerant of names that
HttpRequestHeaders refuses to own, so a content header degrades to "silently ignored" like the other
non-overridable headers — e.g. guard the Remove/Contains pair (try/catch on
InvalidOperationException, or a known-content-header check) and let TryAddWithoutValidation's
false be the no-op it already is. Then extend the XML docs on both CustomHeaders properties to say
content headers are not settable through this option.
Contrast cases that must keep their current behavior:
- non-content custom headers (
X-*, Cache-Control, Referer) still override the defaults — the
Remove must keep working for them;
Accept-Encoding via CustomHeaders keeps its documented precedence (it currently overrides the
client-level value and the driver default, and is in turn outranked by QueryOptions.AcceptEncoding).
Alternatively, throwing an ArgumentException at options validation would also be defensible — it is a
behavior change but a clearer one. Actually routing content headers onto HttpContent.Headers is a
feature rather than a bug fix and is out of scope here.
Configuration
Environment
ClickHouse server
- ClickHouse Server version: 26.5.1.882
- Non-default settings: none
- No tables involved (
SELECT 1).
Found by automated analysis of the client while working on an unrelated compression change, and
verified empirically against a live server (not by inspection alone): all 11 content headers were swept
at both the client and per-query level across four entry points, with non-content headers as controls.
No fix is attached — filing for maintainer triage.
Describe the bug
Setting any of .NET's known content headers via the public
CustomHeadersoption — eitherper-query (
QueryOptions.CustomHeaders) or client-level (ClickHouseClientSettings.CustomHeaders) —makes every request throw before it is sent:
The XML docs on both properties say custom headers "are applied after the default headers, allowing
you to override most headers" and that only
Connection,AuthorizationandUser-Agent"cannot beoverridden and will be silently ignored". A whole class of header names instead escapes as an
unhandled framework exception, which is neither documented nor caught.
All 11 headers that .NET classifies as content headers reproduce it:
Allow,Content-Disposition,Content-Encoding,Content-Language,Content-Length,Content-Location,Content-MD5,Content-Range,Content-Type,Expires,Last-Modified. Non-content headers are unaffected(
X-*,Accept-Encoding,Cache-Control,Refererall work).It affects every entry point that goes through
AddDefaultHttpHeaders— verified onExecuteScalarAsync,ExecuteNonQueryAsync,ExecuteReaderAsyncandExecuteRawResultAsync,at both the client and per-query level.
Steps to reproduce
QueryOptions) with a content header inCustomHeaders.InvalidOperationExceptionbefore any request reaches the server.Expected behaviour
One of the documented outcomes, not an unhandled framework exception:
ArgumentExceptionnaming the header and why.Code example
Error log
Root cause
ClickHouseClient.ApplyCustomHeaders(ClickHouse.Driver/ClickHouseClient.cs:1124-1137) applies everynon-blocked custom header to the request's
HttpRequestHeaders:The throwing call is the
Remove, not the add. Probed directly on .NET 10:HttpRequestHeaders.Remove("Content-Type")→ throwsInvalidOperationException(Containsthrows too)HttpRequestHeaders.TryAddWithoutValidation("Content-Type", …)→ returnsfalse, no throwSo
TryAddWithoutValidationalready degrades gracefully; theRemovethat was added to give customheaders "last write wins" semantics is what turns a no-op into an exception.
IsBlockedHeader(ClickHouseClient.cs:1334-1339) only coversConnection,Authorization,User-Agent.Suggested fix
Smallest change that matches the documented contract: make the removal tolerant of names that
HttpRequestHeadersrefuses to own, so a content header degrades to "silently ignored" like the othernon-overridable headers — e.g. guard the
Remove/Containspair (try/catch onInvalidOperationException, or a known-content-header check) and letTryAddWithoutValidation'sfalsebe the no-op it already is. Then extend the XML docs on bothCustomHeadersproperties to saycontent headers are not settable through this option.
Contrast cases that must keep their current behavior:
X-*,Cache-Control,Referer) still override the defaults — theRemovemust keep working for them;Accept-EncodingviaCustomHeaderskeeps its documented precedence (it currently overrides theclient-level value and the driver default, and is in turn outranked by
QueryOptions.AcceptEncoding).Alternatively, throwing an
ArgumentExceptionat options validation would also be defensible — it is abehavior change but a clearer one. Actually routing content headers onto
HttpContent.Headersis afeature rather than a bug fix and is out of scope here.
Configuration
Environment
main@7b83764(post-fix(insert): buffer uncompressed binary inserts before the request stream #525)net10.0ClickHouse server
SELECT 1).Found by automated analysis of the client while working on an unrelated compression change, and
verified empirically against a live server (not by inspection alone): all 11 content headers were swept
at both the client and per-query level across four entry points, with non-content headers as controls.
No fix is attached — filing for maintainer triage.