ClickHouse / ClickHouse/clickhouse-cs

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
bug
Dominant language
C#
Stars
94
Forks
22
Avg merge
11h 26m
Merged PRs (30d)
22

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

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

```csharp
// per-query
using var client = new ClickHouseClient("Host=localhost");
var options = new QueryOptions
{
CustomHeaders = new Dictionary { ["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 { ["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`:

```csharp
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.

Contributor guide

Open the contributing guide

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.

Assessment

Tech stack
csharp
Domain
api, networking
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.