[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
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 72/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- java
- Área
- backend-api-design
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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.
- Lenguaje dominante
- Java
- Estrellas
- 1.6k
- Forks
- 649
- Merge medio
- 1 d 20 h
- PR fusionados (30 d)
- 39
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de ClickHouse/clickhouse-java
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
ClickHouse/clickhouse-java#3349 ·
Los mantenedores suelen responder en 1 día
-
bug jdbc-v2
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
ClickHouse/clickhouse-java#3347 ·
Los mantenedores suelen responder en 1 día
-
bug jdbc-v2
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
ClickHouse/clickhouse-java#3326 ·
Los mantenedores suelen responder en 1 día
-
[client-v2] Enum8/Enum16 insert accepts a non-member integer and uses Enum.ordinal(), silently storing values that are not enum membersPosiblemente ocupada @jaya232 la tomó hace 6 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
ClickHouse/clickhouse-java#3304 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug jdbc-v2
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
ClickHouse/clickhouse-java#3214 ·
Los mantenedores suelen responder en 1 día
Todos los issues de ClickHouse/clickhouse-java
Issues similares
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
bug:unconfirmed
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
UniversalMediaServer/UniversalMediaServer#6473 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
StarRocks/starrocks#83246 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 2 días