Description
When client-v2 sends statement parameters in the request body (HTTP_SEND_PARAMS_IN_BODY=true, the multipart path) and client request compression is enabled through HTTP compression (compressClientRequest(true) + useHttpCompression(true)), the client sends a plain (uncompressed) multipart body while still declaring Content-Encoding: lz4 and enable_http_compression=1.
The request is therefore self-contradictory. Servers up to 26.3 ignored Content-Encoding for multipart/form-data requests, so this went unnoticed. ClickHouse 26.8 honours the header and rejects the request:
Code: 618. DB::Exception: LZ4 decompression failed. LZ4F version: 100. Error: ERROR_frameType_unknown. (LZ4_DECODER_FAILED)
HttpTransportTests#testMultiPartRequest fails on current main against clickhouse/clickhouse-server:head. It is green in CI only because the CI matrix does not run these legs against head.
Steps to reproduce
- Start
clickhouse/clickhouse-server:head (26.8.1.1761).
- Build current
main (379718d).
- Run
mvn -B -pl client-v2 -DskipUTs=true -Dit.test=HttpTransportTests#testMultiPartRequest -Dfailsafe.failIfNoSpecifiedTests=false verify.
The third block of the test (the one that combines compressClientRequest(true), useHttpCompression(true) and params in body) fails.
The same failure can be produced without the client, which shows exactly what is sent on the wire:
# fails on 26.8 head, succeeds on 25.3 / 25.8 / 26.3
curl -H 'Content-Encoding: lz4' -F 'query=SELECT 1' 'http://server:8123/?enable_http_compression=1'
# succeeds everywhere (same body, header removed)
curl -F 'query=SELECT 1' 'http://server:8123/?enable_http_compression=1'
Error Log or Exception StackTrace
com.clickhouse.client.api.ServerException: Code: 618. DB::Exception: LZ4 decompression failed.
LZ4F version: 100. Error: ERROR_frameType_unknown. (LZ4_DECODER_FAILED) (version 26.8.1.1761 (official build))
at com.clickhouse.client.api.internal.HttpAPIClientHelper.readClickHouseError(HttpAPIClientHelper.java:528)
at com.clickhouse.client.api.internal.HttpAPIClientHelper.readError(HttpAPIClientHelper.java:420)
at com.clickhouse.client.api.internal.HttpAPIClientHelper.doExecuteRequest(HttpAPIClientHelper.java:782)
...
at com.clickhouse.client.api.Client.queryAll(Client.java:2086)
HttpTransportTests.testMultiPartRequest:1831 » Client Failed to get query response
Expected Behaviour
A multipart request must not announce a content encoding that the body does not use. Because the multipart path deliberately does not compress the body, the request must be sent with no Content-Encoding header, and the query must succeed.
Server evidence for the same request, taken with curl (body is plain text in all cases):
| Server |
multipart + Content-Encoding: lz4 |
multipart, no Content-Encoding |
| 25.3.14.14 |
1 (header ignored) |
1 |
| 25.8.28.1 |
1 (header ignored) |
1 |
| 26.3.17.110 |
1 (header ignored) |
1 |
| 26.8.1.1761 (head) |
LZ4_DECODER_FAILED |
1 |
The server behaviour on 26.8 is correct — the client request was always invalid, and the older servers only tolerated it.
Root cause
client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java
The multipart path already knows it cannot compress, but it only neutralises one of the two ways compression is requested.
- Line 642-645 — the body is attached without
wrapRequestEntity(...), so it stays uncompressed:
if (useMultipart) {
req.setEntity(httpEntity); // multipart doesn't support compression right now
} else {
req.setEntity(wrapRequestEntity(httpEntity, requestConfig));
}
- Line 563 — the query parameter form of client compression is removed for multipart:
uriBuilder.removeParameter(ClickHouseHttpProto.QPARAM_DECOMPRESS); // multipart request doesn't support compression yet
- Line 933-936 (
addHeaders) — but the HTTP compression form is not, and the header survives:
if (clientCompression && !appCompressedData) {
setHeader(req, HttpHeaders.CONTENT_ENCODING, DEFAULT_HTTP_COMPRESSION_ALGO); // "lz4"
}
With useHttpCompression(true), addRequestParams sends enable_http_compression=1 instead of decompress=1, so the removeParameter(QPARAM_DECOMPRESS) guard never applies and the Content-Encoding: lz4 header reaches the server together with a plain body.
The mirror configuration (compressClientRequest(true) + useHttpCompression(false)) is unaffected, because there client compression is expressed as decompress=1, which the line-563 guard does remove.
Suggested fix
Extend the existing multipart guard to the header form: in createRequest, when useMultipart is true, also remove HttpHeaders.CONTENT_ENCODING from the request (or skip setting it in addHeaders when the request will be multipart), so the two mitigations stay in step.
Cases that must keep their current behaviour:
- non-multipart +
compressClientRequest(true) + useHttpCompression(true) — body is really LZ4-framed, the header must stay;
- multipart +
compressServerResponse(true) — Accept-Encoding is about the response and must not be removed;
appCompressedData(true) — the header is not set by the client in the first place.
A regression test would be the existing third block of HttpTransportTests#testMultiPartRequest, run against a server that honours the header.
Configuration
Client Configuration
Client client = new Client.Builder()
.addEndpoint(endpoint)
.setUsername(user).setPassword(password)
.compressClientRequest(true)
.useHttpCompression(true)
.setOption(ClientConfigProperties.HTTP_SEND_PARAMS_IN_BODY.getKey(), "true")
.build();
client.queryAll("SELECT database, name FROM system.tables WHERE name IN {table_names:Array(String)}", params);
Environment
ClickHouse Server
- ClickHouse Server version: 26.8.1.1761 (
clickhouse/clickhouse-server:head); not reproducible on 26.3.17.110, 25.8.28.1, 25.3.14.14
- Non-default settings: none
- Tables: none — reproduces with
SELECT 1 and with system.tables
Found by automated analysis of client-v2 while working on #3068 / #3069, and verified against live servers of four versions rather than by inspection.
Description
When client-v2 sends statement parameters in the request body (
HTTP_SEND_PARAMS_IN_BODY=true, the multipart path) and client request compression is enabled through HTTP compression (compressClientRequest(true)+useHttpCompression(true)), the client sends a plain (uncompressed) multipart body while still declaringContent-Encoding: lz4andenable_http_compression=1.The request is therefore self-contradictory. Servers up to 26.3 ignored
Content-Encodingformultipart/form-datarequests, so this went unnoticed. ClickHouse 26.8 honours the header and rejects the request:Code: 618. DB::Exception: LZ4 decompression failed. LZ4F version: 100. Error: ERROR_frameType_unknown. (LZ4_DECODER_FAILED)HttpTransportTests#testMultiPartRequestfails on currentmainagainstclickhouse/clickhouse-server:head. It is green in CI only because the CI matrix does not run these legs againsthead.Steps to reproduce
clickhouse/clickhouse-server:head(26.8.1.1761).main(379718d).mvn -B -pl client-v2 -DskipUTs=true -Dit.test=HttpTransportTests#testMultiPartRequest -Dfailsafe.failIfNoSpecifiedTests=false verify.The third block of the test (the one that combines
compressClientRequest(true),useHttpCompression(true)and params in body) fails.The same failure can be produced without the client, which shows exactly what is sent on the wire:
Error Log or Exception StackTrace
Expected Behaviour
A multipart request must not announce a content encoding that the body does not use. Because the multipart path deliberately does not compress the body, the request must be sent with no
Content-Encodingheader, and the query must succeed.Server evidence for the same request, taken with
curl(body is plain text in all cases):Content-Encoding: lz4Content-Encoding1(header ignored)11(header ignored)11(header ignored)1LZ4_DECODER_FAILED1The server behaviour on 26.8 is correct — the client request was always invalid, and the older servers only tolerated it.
Root cause
client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.javaThe multipart path already knows it cannot compress, but it only neutralises one of the two ways compression is requested.
wrapRequestEntity(...), so it stays uncompressed:addHeaders) — but the HTTP compression form is not, and the header survives:With
useHttpCompression(true),addRequestParamssendsenable_http_compression=1instead ofdecompress=1, so theremoveParameter(QPARAM_DECOMPRESS)guard never applies and theContent-Encoding: lz4header reaches the server together with a plain body.The mirror configuration (
compressClientRequest(true)+useHttpCompression(false)) is unaffected, because there client compression is expressed asdecompress=1, which the line-563 guard does remove.Suggested fix
Extend the existing multipart guard to the header form: in
createRequest, whenuseMultipartis true, also removeHttpHeaders.CONTENT_ENCODINGfrom the request (or skip setting it inaddHeaderswhen the request will be multipart), so the two mitigations stay in step.Cases that must keep their current behaviour:
compressClientRequest(true)+useHttpCompression(true)— body is really LZ4-framed, the header must stay;compressServerResponse(true)—Accept-Encodingis about the response and must not be removed;appCompressedData(true)— the header is not set by the client in the first place.A regression test would be the existing third block of
HttpTransportTests#testMultiPartRequest, run against a server that honours the header.Configuration
Client Configuration
Environment
main, 379718d)ClickHouse Server
clickhouse/clickhouse-server:head); not reproducible on 26.3.17.110, 25.8.28.1, 25.3.14.14SELECT 1and withsystem.tablesFound by automated analysis of client-v2 while working on #3068 / #3069, and verified against live servers of four versions rather than by inspection.