CustomHeaders: any HTTP content header throws InvalidOperationException ("Misused header name") instead of being applied or ignored
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- csharp
- Domain
- api, networking
Research direction
Start in ClickHouse.Driver/ClickHouseClient.cs at ApplyCustomHeaders (lines 1124-1137) and inspect IsBlockedHeader (lines 1334-1339), then reproduce the failure with the listed client-level and per-query entry points. Done means content headers no longer cause an unhandled InvalidOperationException, non-content overrides and Accept-Encoding precedence remain intact, and both CustomHeaders XML docs describe the behavior.
Written by the indexing model from the issue text.
Description
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 inCustomHeaders. - Run any query.
- The call throws
InvalidOperationExceptionbefore 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
ArgumentExceptionnaming 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")→ throwsInvalidOperationException(Containsthrows too)HttpRequestHeaders.TryAddWithoutValidation("Content-Type", …)→ returnsfalse, 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
Removemust keep working for them; Accept-EncodingviaCustomHeaderskeeps its documented precedence (it currently overrides the
client-level value and the driver default, and is in turn outranked byQueryOptions.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
- Client version:
main@7b83764(post-#525) - .NET version: SDK 10.0.302, test target
net10.0 - OS: Ubuntu 24.04 (x64, container)
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.
- Dominant language
- C#
- Stars
- 94
- Forks
- 22
- Avg merge
- 9h 32m
- Merged PRs (30d)
- 14
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ClickHouse/clickhouse-cs
-
bug
Difficulty 1/5 Under an hour Newbie friendliness 85/100
ClickHouse/clickhouse-cs#545 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/clickhouse-cs#505 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
ClickHouse/clickhouse-cs#316 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 78/100
ClickHouse/clickhouse-cs#618 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 78/100
ClickHouse/clickhouse-cs#614 ·
All issues in ClickHouse/clickhouse-cs
Similar issues
-
type/automation type/tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
t/bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
ci-failure-cause test-failure
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
area:auth FE mvp P3
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
klasolsson81/jobbliggaren#1788 ·