[client-v2] Error body without X-ClickHouse-Exception-Code is cut short: only the first chunk is read, and short bodies are lost with response compression
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 72/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- java
調査の方向性
Start with HttpAPIClientHelper.readNotClickHouseError and the related CompressedBlockInputStream.readFully behavior described in the issue. Reproduce the short-body and multi-chunk cases with compress=true and compress=false, then inspect tests around error response handling and the existing PR #3322 test. Done when error messages include the full plain-text body up to 1024 bytes while successful responses and errors with an exception-code header retain their existing behavior.
索引モデルが issue の本文から書いたものです。
説明
Description
HttpAPIClientHelper.readNotClickHouseError makes the ServerException message for an error response (400, 401, 403, 404, 500) that has no X-ClickHouse-Exception-Code header. Examples: a proxy or load-balancer error page, or the ClickHouse server's own There is no handle /... 404. The method does not read the full body. On main (f9586aba0) there are three defects:
- Only one
read()on the raw stream.InputStream.read()can return fewer bytes than are available. Apache HttpCore 5ChunkedInputStreamreturns at most the rest of the current chunk. So a body sent withTransfer-Encoding: chunkedin more than one chunk is cut at the end of the first chunk, also when all bytes arrive in one TCP write. This occurs withcompress=trueand withcompress=false. - With
compress=true(the default), the last 25 bytes of the read are lost. After the "Invalid LZ4 magic byte" fallback, the 25 header bytes are inbuffer[0..25)and the next read writes tobuffer[25..]. But the message isnew String(buffer, 0, msgLen), which ignoresoffset. A body of exactly 25 bytes givesunknown server error. - With
compress=true, a body shorter than 25 bytes is lost.CompressedBlockInputStream.readFullythrowsIOException("Incomplete read: 18 of 25"), not aClientException. The generic catch logs a WARN with a stack trace, and the message isunknown server error. Example: the 18-byte body404 page not found.
Open PR #3322 fixes defect 2: I ran the same reproduction on its head (a7f13225e). Defects 1 and 3 still occur on that head. Note: #3322 adds testTruncatedCompressedErrorBodyStaysUnreadable, which intentionally keeps a body shorter than 25 bytes "unreadable" for a response with an exception code. A response without the header comes from a proxy or a non-query handler, so its body is plain text, not a truncated compressed block.
Steps to reproduce
- Point client-v2 at an HTTP endpoint that returns 404 for
POST /with a plain-text body and noX-ClickHouse-Exception-Codeheader. A small socket server or WireMock is sufficient (see the code example). For defect 2, a real ClickHouse server with a wrong endpoint path (http://host:8123/nope) also works. - Call
client.query("SELECT 1").get(). - Compare
ServerException.getMessage()with the body that was sent.
Error Log or Exception StackTrace
Results on main f9586aba0:
| Body sent | compress=true (default) |
compress=false |
|---|---|---|
404 page not found (18 B) |
unknown server error (transport error: 404) + WARN below |
full body |
| 25 B | unknown server error |
full body |
| 26 B | a (1 char) |
full body |
| 60 B | first 35 chars | full body |
| nginx 404 HTML page (144 B) | ends at <hr><center>nginx</ |
full body |
| 129 B in 3 chunks, one TCP write | upstream connect e (18 chars) |
first chunk only (43 chars) |
ClickHouse 26.9.1.1629 404 There is no handle /nope |
last 25 bytes lost: the last line is clickhouse instead of clickhouse-client < query > result |
full body |
On PR #3322 head a7f13225e: rows 2-5 and 7 are correct. Row 1 is still unknown server error. Row 6 is still the first chunk only, in both modes.
WARN for row 1:
WARN HttpAPIClientHelper - Failed to read error message (queryId = )
java.io.IOException: Incomplete read: 18 of 25
at com.clickhouse.client.api.internal.CompressedBlockInputStream.readFully(CompressedBlockInputStream.java:89)
at com.clickhouse.client.api.internal.CompressedBlockInputStream.refill(CompressedBlockInputStream.java:108)
at com.clickhouse.client.api.internal.CompressedBlockInputStream.read(CompressedBlockInputStream.java:63)
at com.clickhouse.client.api.internal.HttpAPIClientHelper.readNotClickHouseError(HttpAPIClientHelper.java:459)
at com.clickhouse.client.api.internal.HttpAPIClientHelper.readError(HttpAPIClientHelper.java:439)
at com.clickhouse.client.api.internal.HttpAPIClientHelper.doExecuteRequest(HttpAPIClientHelper.java:868)
Expected Behaviour
The ServerException message contains the full body text, up to ERROR_BODY_BUFFER_SIZE (1024 bytes). This is what compress=false gives today for a body that arrives in one chunk.
Code Example
Chunked case (row 6). The server writes the whole response in one write(), so the result does not depend on timing:
String[] chunks = {
"upstream connect error or disconnect/reset ",
"before headers. reset reason: connection ",
"failure, transport failure reason: marker-END"};
StringBuilder rsp = new StringBuilder("HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n"
+ "Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n");
for (String c : chunks) {
rsp.append(Integer.toHexString(c.length())).append("\r\n").append(c).append("\r\n");
}
rsp.append("0\r\n\r\n");
ServerSocket ss = new ServerSocket(0);
new Thread(() -> {
try (Socket s = ss.accept()) {
// read the request headers up to "\r\n\r\n" (omitted), then:
s.getOutputStream().write(rsp.toString().getBytes(StandardCharsets.UTF_8));
s.shutdownOutput();
// drain the request body (omitted)
} catch (Exception ignore) { }
}).start();
try (Client client = new Client.Builder()
.addEndpoint(Protocol.HTTP, "localhost", ss.getLocalPort(), false)
.setUsername("default").setPassword("x")
.build()) { // add .compressServerResponse(false) for the contrast column
client.query("SELECT 1").get(10, TimeUnit.SECONDS);
} catch (ServerException e) {
System.out.println(e.getMessage()); // "upstream connect e (transport error: 404)"
}
Rows 1-5 use the same client with a WireMock stub: aResponse().withStatus(404).withBody(<body>), no X-ClickHouse-Exception-Code header.
Root cause
HttpAPIClientHelper.java:459: onebody.read(buffer, offset, buffer.length - offset). There is no loop until EOF or until the buffer is full (defect 1).HttpAPIClientHelper.java:461:new String(buffer, 0, msgLen, ...)ignoresoffset, which is set to 25 at line 473 (defect 2).CompressedBlockInputStream.java:89:readFullythrowsIOExceptionwhen the body ends before 25 bytes. The fallback atHttpAPIClientHelper.java:467catches onlyClientException, so lines 478-480 discard the bytes that were already read (defect 3).
Suggested fix
- Read in a loop until EOF or until the buffer is full, and build the message from
offset + total bytes read. - For defect 3, two possible approaches:
wrapResponseEntitydoes not putCompressedBlockEntityaround an error response withoutX-ClickHouse-Exception-Code. It already skips the wrapper for 401/403. Then the header fallback is not necessary on this path.- Or
CompressedBlockInputStreamrecords how many header bytes it read before the stream ended, andreadNotClickHouseErroruses those bytes as plain text.
- Keep the current behavior for successful responses and for error responses that have the exception-code header: they still go through
CompressedBlockInputStream.
Configuration
Client Configuration
new Client.Builder()
.addEndpoint(Protocol.HTTP, "localhost", port, false)
.setUsername("default").setPassword("x")
.build(); // defaults: compress=true, use_http_compression=false
Environment
- Cloud
- Client version:
mainf9586aba0 (0.12.0-rc1-SNAPSHOT); also checked on PR #3322 head a7f13225e - Language version: Java 17 (Eclipse Temurin), httpcore5 5.4.3
- OS: Linux (Docker)
ClickHouse Server
- ClickHouse Server version: 26.9.1.1629 (only the real-server row; the other rows use a mock HTTP server)
- ClickHouse Server non-default settings, if any: none
CREATE TABLEstatements for tables involved: none- Sample data for all these tables: none
Found by automated analysis (Polyglot AI) while working on #2287 (PR #3335, which changes only readClickHouseError). Verified by running the client against a mock HTTP server, a raw socket server, and a live ClickHouse 26.9.1.1629 server, not only by reading the code.
- 主要言語
- Java
- スター
- 1.6k
- フォーク
- 649
- 平均マージ
- 1日 20時間
- マージ済み PR(30日)
- 39
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
ClickHouse/clickhouse-java のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
ClickHouse/clickhouse-java#3349 ·
メンテナーはふだん 1 日以内に返信
-
bug jdbc-v2
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
ClickHouse/clickhouse-java#3347 ·
メンテナーはふだん 1 日以内に返信
-
bug jdbc-v2
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
ClickHouse/clickhouse-java#3326 ·
メンテナーはふだん 1 日以内に返信
-
[client-v2] Enum8/Enum16 insert accepts a non-member integer and uses Enum.ordinal(), silently storing values that are not enum members対応中かも @jaya232 が 6 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
ClickHouse/clickhouse-java#3304 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug jdbc-v2
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
ClickHouse/clickhouse-java#3214 ·
メンテナーはふだん 1 日以内に返信
ClickHouse/clickhouse-java の issue をすべて見る
似ている issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
bug:unconfirmed
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
UniversalMediaServer/UniversalMediaServer#6473 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
StarRocks/starrocks#83246 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 2 日以内に返信