CustomHeaders: any HTTP content header throws InvalidOperationException ("Misused header name") instead of being applied or ignored

Open Beginner friendly
#528 0 comments 0 reactions 0 assignees View on GitHub

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

bug
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
  1. Create a client (or QueryOptions) with a content header in CustomHeaders.
  2. Run any query.
  3. 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
  • 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from ClickHouse/clickhouse-cs

All issues in ClickHouse/clickhouse-cs

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.