You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When the ClickHouse server returns HTTP 200 OK while reporting an error (mid-stream exception, distributed-DDL error blocks, etc.), ClickHouseClient.ExecuteNonQueryAsync silently treats the response as success. Two factors combine to produce this:
ClickHouseClient.HandleError (ClickHouse.Driver/ClickHouseClient.cs:889-901) decides success/failure purely from response.IsSuccessStatusCode. It never inspects X-ClickHouse-Exception-Code (or X-ClickHouse-Exception-Tag) on the response. Any 2xx response — even one carrying an error indicator in the headers — is treated as a success and returned to the caller.
ExecuteNonQueryAsync (ClickHouse.Driver/ClickHouseClient.cs:208-218) does not run the body through the existing mid-stream exception detection. It opens the raw response stream and reads a single 7-bit-encoded int as the affected-rows count:
Net effect: a DDL/INSERT that the server reports as failed via HTTP 200 + exception headers + body-encoded error is observable by the application as success. This risks silent schema-change failure or silent data loss.
The ClickHouseDataReader path is partially protected because it wraps the stream in ExceptionTagAwareStream when X-ClickHouse-Exception-Tag is present, but it still does not consult X-ClickHouse-Exception-Code and will not detect cases where the server has encoded the error inside a valid Native/RowBinary block (no __exception__ marker), e.g. the distributed-DDL scenario from clickhouse-go#1398.
ClickHouse server version
26.4.2.10 (local server). Verified against this server that HTTP 200 responses with X-ClickHouse-Exception-Tag (and an error body) are produced when streaming has begun before the error is detected — for example:
Note that for this mid-stream case the server emits X-ClickHouse-Exception-Tag but not X-ClickHouse-Exception-Code; for fully buffered failures it emits both alongside HTTP 500. The cross-shard distributed-DDL case (clickhouse-go#1398) produces an HTTP 200 response whose body encodes the error inside a Native block, with no __exception__ marker — this is not reproducible against a single-node server.
Reproduction
The HTTP-status-only check is straightforward to demonstrate without a server using a stub that mirrors what the server emits during a mid-stream exception. Add the following to ClickHouse.Driver.Tests (alongside MidStreamExceptionTests):
usingSystem.Net;usingSystem.Net.Http;usingSystem.Threading.Tasks;usingClickHouse.Driver;usingNUnit.Framework;namespaceClickHouse.Driver.Tests.ADO;publicclassExecuteNonQuerySilentExceptionTests{privatesealedclassStubHandler:HttpMessageHandler{protectedoverrideTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,System.Threading.CancellationTokenct){// Mimic the server's mid-stream exception: HTTP 200 + exception headers + body.varresp=newHttpResponseMessage(HttpStatusCode.OK){Content=newByteArrayContent(System.Text.Encoding.UTF8.GetBytes("Code: 44. DB::Exception: Sorting key contains nullable columns ... (ILLEGAL_COLUMN)"))};resp.Headers.TryAddWithoutValidation("X-ClickHouse-Exception-Code","44");resp.Headers.TryAddWithoutValidation("X-ClickHouse-Exception-Tag","abc");returnTask.FromResult(resp);}}[Test]publicasyncTaskExecuteNonQueryAsync_WhenHttp200ButExceptionHeaderSet_ShouldThrow(){varsettings=newClickHouseClientSettings("Host=localhost"){HttpClient=newHttpClient(newStubHandler()),};usingvarclient=newClickHouseClient(settings);Assert.ThrowsAsync<ClickHouseServerException>(async()=>awaitclient.ExecuteNonQueryAsync("CREATE TABLE t ON CLUSTER c (...) ENGINE = ReplicatedMergeTree PRIMARY KEY (a)"));}}
Expected: ExecuteNonQueryAsync throws ClickHouseServerException (the server reported an exception via header).
Actual: ExecuteNonQueryAsync returns 0 and no exception is raised.
The same shape applies to the streaming reader path for the distributed-DDL case where the body is a valid Native block whose rows encode the error text — ClickHouseDataReader decodes the rows without seeing an __exception__ marker, and X-ClickHouse-Exception-Code is not consulted.
Suggested fix
In ClickHouseClient.HandleError, after the IsSuccessStatusCode short-circuit, also inspect the response for X-ClickHouse-Exception-Code (or X-ClickHouse-Exception-Tag from Add support for mid-stream exception detection via X-ClickHouse-Exception-Tag header #168). If either is present with a non-empty value, treat the response as an error regardless of status code: read the body and throw a ClickHouseServerException with the parsed code/message. This single change covers ExecuteNonQueryAsync, ExecuteScalarAsync, ExecuteReaderAsync, ExecuteRawResultAsync, PostStreamAsync, PingAsync, and the insert/bulk paths because they all route through HandleError.
Optionally, default wait_end_of_query=1 for the Exec/Ping paths (where streaming the response is not needed) so the server emits a proper non-2xx status when an exception is detected before the body is flushed.
Add the unit test above to ClickHouse.Driver.Tests/ADO/MidStreamExceptionTests.cs (it does not require a running server).
Description
When the ClickHouse server returns
HTTP 200 OKwhile reporting an error (mid-stream exception, distributed-DDL error blocks, etc.),ClickHouseClient.ExecuteNonQueryAsyncsilently treats the response as success. Two factors combine to produce this:ClickHouseClient.HandleError(ClickHouse.Driver/ClickHouseClient.cs:889-901) decides success/failure purely fromresponse.IsSuccessStatusCode. It never inspectsX-ClickHouse-Exception-Code(orX-ClickHouse-Exception-Tag) on the response. Any 2xx response — even one carrying an error indicator in the headers — is treated as a success and returned to the caller.ExecuteNonQueryAsync(ClickHouse.Driver/ClickHouseClient.cs:208-218) does not run the body through the existing mid-stream exception detection. It opens the raw response stream and reads a single 7-bit-encoded int as the affected-rows count:The
ExceptionTagAwareStreamwrapper that detects__exception__markers whenX-ClickHouse-Exception-Tagis present (added in Add support for mid-stream exception detection via X-ClickHouse-Exception-Tag header #168) is only wired up insideClickHouseDataReader.FromHttpResponseAsync.ExecuteNonQueryAsyncbypasses it entirely.Net effect: a DDL/INSERT that the server reports as failed via
HTTP 200+ exception headers + body-encoded error is observable by the application as success. This risks silent schema-change failure or silent data loss.The
ClickHouseDataReaderpath is partially protected because it wraps the stream inExceptionTagAwareStreamwhenX-ClickHouse-Exception-Tagis present, but it still does not consultX-ClickHouse-Exception-Codeand will not detect cases where the server has encoded the error inside a valid Native/RowBinary block (no__exception__marker), e.g. the distributed-DDL scenario from clickhouse-go#1398.ClickHouse server version
26.4.2.10(local server). Verified against this server thatHTTP 200responses withX-ClickHouse-Exception-Tag(and an error body) are produced when streaming has begun before the error is detected — for example:Note that for this mid-stream case the server emits
X-ClickHouse-Exception-Tagbut notX-ClickHouse-Exception-Code; for fully buffered failures it emits both alongsideHTTP 500. The cross-shard distributed-DDL case (clickhouse-go#1398) produces anHTTP 200response whose body encodes the error inside a Native block, with no__exception__marker — this is not reproducible against a single-node server.Reproduction
The HTTP-status-only check is straightforward to demonstrate without a server using a stub that mirrors what the server emits during a mid-stream exception. Add the following to
ClickHouse.Driver.Tests(alongsideMidStreamExceptionTests):Expected:
ExecuteNonQueryAsyncthrowsClickHouseServerException(the server reported an exception via header).Actual:
ExecuteNonQueryAsyncreturns0and no exception is raised.The same shape applies to the streaming reader path for the distributed-DDL case where the body is a valid Native block whose rows encode the error text —
ClickHouseDataReaderdecodes the rows without seeing an__exception__marker, andX-ClickHouse-Exception-Codeis not consulted.Suggested fix
ClickHouseClient.HandleError, after theIsSuccessStatusCodeshort-circuit, also inspect the response forX-ClickHouse-Exception-Code(orX-ClickHouse-Exception-Tagfrom Add support for mid-stream exception detection via X-ClickHouse-Exception-Tag header #168). If either is present with a non-empty value, treat the response as an error regardless of status code: read the body and throw aClickHouseServerExceptionwith the parsed code/message. This single change coversExecuteNonQueryAsync,ExecuteScalarAsync,ExecuteReaderAsync,ExecuteRawResultAsync,PostStreamAsync,PingAsync, and the insert/bulk paths because they all route throughHandleError.wait_end_of_query=1for theExec/Pingpaths (where streaming the response is not needed) so the server emits a proper non-2xx status when an exception is detected before the body is flushed.ClickHouse.Driver.Tests/ADO/MidStreamExceptionTests.cs(it does not require a running server).Link
Related upstream issue: ClickHouse/clickhouse-rs#255
Related sibling-client manifestation: ClickHouse/clickhouse-go#1398