ClickHouse / ClickHouse/clickhouse-cs
CustomHeaders: any HTTP content header throws InvalidOperationException ("Misused header name") instead of being applied or ignored
- 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
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