[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
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức phù hợp với người mới
- 72/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- java
- Lĩnh vực
- backend-api-design
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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.
- Ngôn ngữ chính
- Java
- Star
- 1.6k
- Fork
- 649
- Merge trung bình
- 1 ngày 20 giờ
- Pull request đã merge (30 ngày)
- 39
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của ClickHouse/clickhouse-java
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
ClickHouse/clickhouse-java#3349 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug jdbc-v2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
ClickHouse/clickhouse-java#3347 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug jdbc-v2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
ClickHouse/clickhouse-java#3326 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[client-v2] Enum8/Enum16 insert accepts a non-member integer and uses Enum.ordinal(), silently storing values that are not enum membersCó thể đã có người làm @jaya232 đã nhận 6 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
ClickHouse/clickhouse-java#3304 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug jdbc-v2
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
ClickHouse/clickhouse-java#3214 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của ClickHouse/clickhouse-java
Issue tương tự
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
bug:unconfirmed
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
UniversalMediaServer/UniversalMediaServer#6473 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
StarRocks/starrocks#83246 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 2 ngày