fix(0281): make ClickHouse errors readable — they were all empty - #310
Merged
karczuRF merged 1 commit intoSep 11, 2026
Merged
Conversation
Every ClickHouse error reaching production was `BadResponse("")`. Not only
timeouts: unknown table, syntax error, quota exceeded and disk-full all
arrived with the code and the message stripped off, indistinguishable from a
network blip. That is the exact signature that hid 0215's outage for 26 days,
and it was still live after the fix written to end it.
The cause is a fallback that does not fire. `collect_bad_response` LZ4-decodes
the error body and falls back to the raw bytes on failure:
let bytes = collect_bytes(stream).await.unwrap_or(raw_bytes);
Straight to ClickHouse the decode FAILS, the fallback fires, the message
survives. Through a proxy the chunk reframing makes the decode SUCCEED with
zero bytes — Ok(empty) — so unwrap_or never runs and the message becomes "".
Every production client reaches ClickHouse through Caddy, so every production
error was empty. Our client never set compression, so it ran the crate default
of Lz4.
Measured against CH 26.3.10.60, same statement three ways:
direct "bad response: Code: 159. DB::Exception:
Timeout exceeded: elapsed 1000.178179 ms,
maximum: 1000 ms. (TIMEOUT_EXCEEDED)"
through Caddy "bad response: "
through Caddy, no LZ4 the full message returns
The fix is one setting, but it is placed in a named shared function rather
than inline so the test exercises the same code production does: every client
this crate builds goes through `with_readable_errors`.
The test requires a reverse proxy and FAILS when CLICKHOUSE_PROXY_URL is
unset, rather than skipping. Without a proxy it would pass whether the defect
is present or not, and a test that cannot fail is worse than no test —
scripts/ch-proxy-0281.sh stands one up. Verified red before the fix with the
exact 0281 message, green after.
Trade-off, stated: response bandwidth on reads. Worth paying against errors
that cannot be diagnosed at all. Revisit if the crate fixes the fallback
upstream — we are pinned to 0.13.3 and 0.15.x is unexamined.
karczuRF
added a commit
that referenced
this pull request
Sep 11, 2026
PR #310 is on develop and nothing in production has changed. Records the scope decision the fix forces: with_readable_errors sits in the single client funnel, so the benefit is repo-wide but the stacks deploy separately — and the enrichment worker is in EventBridge, not Compute, which cost a wasted step on 0215's run. The acceptance is 0215's induction re-run: the same failure must now arrive as Code: 159 TIMEOUT_EXCEEDED instead of an empty body.
karczuRF
deleted the
fix/0281_clickhouse-client-discards-the-exception-body
branch
September 11, 2026 13:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
BadResponse("")— an empty string. Not only timeouts: unknown table, syntax error, quota exceeded and disk-full all arrived with the code and message stripped off, indistinguishable from a network blip.with_readable_errors.scripts/ch-proxy-0281.shto stand up the proxy it needs.How it was found
0215's acceptance criterion said "verified by inducing, not inferred". The induction on 2026-09-11 failed: the bound fired exactly (ClickHouse killed the statement at 1002.3 ms against 1000 ms) but the worker logged
clickhouse: bad response:. Everything needed to diagnose the failure existed on the wire and was lost one layer above it.The mechanism
collect_bad_responseLZ4-decodes the error body and falls back to the raw bytes on failure:Straight to ClickHouse the decode fails, the fallback fires, the message survives. Through a proxy the chunk reframing makes the decode succeed with zero bytes —
Ok(empty)— sounwrap_ornever runs. Every production client reaches ClickHouse through Caddy, and our client never set compression, so it ran the crate default ofLz4.Measured against CH 26.3.10.60, the same statement three ways:
bad response: Code: 159. DB::Exception: Timeout exceeded: elapsed 1000.178179 ms, maximum: 1000 ms. (TIMEOUT_EXCEEDED)bad response:curlsees the body in all three — it's specific to the crate's decode path.Why the fix lives in a named function
One setting, but placed in
with_readable_errorsrather than inline, so the test exercises the same code production does.mtls::client_with_mtls— the single funnel every worker, probe, API and operator CLI goes through — calls it.The test fails when its proxy is missing, deliberately
Straight to ClickHouse this test passes whether or not the defect is present, because the fallback fires. So
CLICKHOUSE_PROXY_URLbeing unset is an error, not a skip — a test that cannot fail is worse than no test, because it reports success.scripts/ch-proxy-0281.sh up CLICKHOUSE_PROXY_URL=http://localhost:8124 \ cargo test -p prices-clickhouse --test execution_bound_error_it -- --ignoredVerified red before the fix, with the exact message
the error carries no message at all — this is the 0281 defect: "bad response: ", and green after.Trade-off, stated
Response bandwidth on reads, since ClickHouse no longer compresses result bodies. Worth paying against errors that cannot be diagnosed at all. Revisit if the crate fixes the fallback upstream — we are pinned to
0.13.3and0.15.xis unexamined.Closes
0215's last acceptance criterion depends on this. Once deployed, the induction can be re-run and should produce
TIMEOUT_EXCEEDED(159) instead of an empty body.