Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
45ea0e4
fix(read): detect mid-stream server exceptions on the streaming read …
polyglotAI-bot Jul 30, 2026
f1d3498
Fix ClickHouseRawResult: surface in-band mid-stream server exceptions…
polyglotAI-bot Jul 30, 2026
605f844
Merge origin/main into polyglot/fix-cs475-raw-midstream-exception
polyglotAI-bot Jul 31, 2026
2384a76
Address review feedback: integration tests for the raw mid-stream sur…
polyglotAI-bot Jul 31, 2026
e798743
Serve buffered body from the raw streaming accessors after a buffered…
polyglotAI-bot Jul 31, 2026
7b89e2b
Throw like untagged HttpContent when re-materializing a consumed raw …
polyglotAI-bot Jul 31, 2026
2d4ff8d
Merge remote-tracking branch 'origin/main' into polyglot/fix-cs475-ra…
polyglotAI-bot Aug 4, 2026
9a646a8
Merge remote-tracking branch 'origin/main' into polyglot/fix-cs475-ra…
polyglotAI-bot Aug 5, 2026
7334233
Merge remote-tracking branch 'origin/main' into polyglot/fix-cs475-ra…
polyglotAI-bot Aug 5, 2026
f7b1591
Merge remote-tracking branch 'origin/main' into polyglot/fix-cs475-ra…
polyglotAI-bot Aug 5, 2026
f64fe3a
fix(raw): detect in-band mid-stream exceptions on compressed bodies
polyglotAI-bot Aug 5, 2026
c8a0f80
Merge remote-tracking branch 'origin/main' into polyglot/fix-cs475-ra…
polyglotAI-bot Aug 5, 2026
f8e9b74
Merge remote-tracking branch 'origin/main' into polyglot/fix-cs475-ra…
polyglotAI-bot Aug 14, 2026
dd9ff64
test(raw): use snappy for the undecodable-codec case now that zstd de…
polyglotAI-bot Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,93 @@ public async Task RawResultDispose_DisposesTheDecoderItInserted()
Assert.Throws<ObjectDisposedException>(() => decoder.ReadByte(), "the raw result owns the decoder it created");
}

/// <summary>
/// A response carrying <c>X-ClickHouse-Exception-Tag</c> — sent whenever
/// <c>http_write_exception_in_output_format</c> is on, i.e. on every response of such a query, failing or
/// not. It engages the in-band exception scanner, and with it the single-consumption bookkeeping the
/// verbatim members already do, which the tests below extend to the decoding member.
/// </summary>
private static HttpResponseMessage CreateTaggedStreamedResponse(byte[] body, string contentEncoding)
{
var response = CreateStreamedResponse(body, contentEncoding);
response.Headers.Add("X-ClickHouse-Exception-Tag", "PU1FNUFH98");
return response;
}

[Test]
public async Task ReadDecompressedStreamAsync_OnATaggedResponse_AfterAPartialVerbatimRead_ThrowsLikeConsumedContent()
{
// With the scanner engaged, a verbatim read marks the content stream consumed. Decoding it from
// there would start mid-frame, so this must fail the same way the re-materializing members do —
// rather than surfacing a decoder-internal error, or worse a short body.
using var response = CreateTaggedStreamedResponse(Lz4Encoded(), "lz4");
using var raw = new ClickHouseRawResult(response);

(await raw.ReadAsStreamAsync()).ReadByte();

Assert.ThrowsAsync<InvalidOperationException>(() => raw.ReadDecompressedStreamAsync());
}

[Test]
public async Task ReadAsByteArrayAsync_OnATaggedResponse_AfterAPartialDecode_ThrowsRatherThanTruncating()
{
// The mirror image: the decoding member consumes the content stream too (and reads ahead), so a
// member that has to re-materialize the whole body afterwards must fail loudly instead of caching
// whatever bytes are left.
using var response = CreateTaggedStreamedResponse(Lz4Encoded(), "lz4");
using var raw = new ClickHouseRawResult(response);

Assert.That((await raw.ReadDecompressedStreamAsync()).ReadByte(), Is.EqualTo(Plaintext[0]));

Assert.ThrowsAsync<InvalidOperationException>(() => raw.ReadAsByteArrayAsync());
}

[Test]
public async Task ReadDecompressedStreamAsync_OnATaggedResponse_AfterABufferingMember_StillSeesTheWholeBody()
{
// A buffering member on the tagged path caches the body itself — still encoded, since it hands the
// wire bytes over verbatim — so the decode must run over that buffer rather than the content stream
// it drained.
using var response = CreateTaggedStreamedResponse(Lz4Encoded(), "lz4");
using var raw = new ClickHouseRawResult(response);

Assert.That(await raw.ReadAsByteArrayAsync(), Is.EqualTo(Lz4Encoded()));

using var buffer = new MemoryStream();
await (await raw.ReadDecompressedStreamAsync()).CopyToAsync(buffer);

Assert.That(buffer.ToArray(), Is.EqualTo(Plaintext));
}

[Test]
public async Task ReadDecompressedStreamAsync_OnATaggedResponse_WithUnsupportedCodec_LeavesTheBodyReadable()
{
// The undecodable-codec throw hands nothing out, so — exactly as on an untagged response — it must
// not mark the content consumed and lock the other members out of a body that is still whole.
// snappy, not zstd: zstd became decodable when the vendored ZstdSharp codec landed.
var compressed = Lz4Encoded();
using var response = CreateTaggedStreamedResponse(compressed, "snappy");
using var raw = new ClickHouseRawResult(response);

Assert.ThrowsAsync<NotSupportedException>(() => raw.ReadDecompressedStreamAsync());

Assert.That(await raw.ReadAsByteArrayAsync(), Is.EqualTo(compressed));
}

[Test]
public async Task ReadDecompressedStreamAsync_OnATaggedResponse_CalledTwice_ReturnsTheSameStream()
{
// Repeat calls must hand back the same scanner: a fresh one would have observed nothing, so the
// marker recorded so far — the whole point of the wrapper — would be lost.
using var response = CreateTaggedStreamedResponse(Lz4Encoded(), "lz4");
using var raw = new ClickHouseRawResult(response);

var first = await raw.ReadDecompressedStreamAsync();
var second = await raw.ReadDecompressedStreamAsync();

Assert.That(second, Is.SameAs(first));
}

[Test]
public async Task RawResultDispose_AfterTheCallerAlreadyDisposedTheDecoder_IsHarmless()
{
Expand Down
Loading
Loading