Skip to content

Commit 897edce

Browse files
authored
Merge pull request #1907 from ClickHouse/v2_fix_multiline_error_msg
[client-v2] Fixed reading server errors messages if they are multi-line
2 parents b08a38f + 9744346 commit 897edce

File tree

5 files changed

+159
-43
lines changed

5 files changed

+159
-43
lines changed

clickhouse-client/src/test/java/com/clickhouse/client/ClickHouseServerForTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ public class ClickHouseServerForTest {
172172
ClickHouseProtocol.TCP.getDefaultSecurePort(),
173173
ClickHouseProtocol.POSTGRESQL.getDefaultPort())
174174
.withClasspathResourceMapping("containers/clickhouse-server", customDirectory, BindMode.READ_ONLY)
175+
.withClasspathResourceMapping("empty.csv", "/var/lib/clickhouse/user_files/empty.csv", BindMode.READ_ONLY)
175176
.withFileSystemBind(System.getProperty("java.io.tmpdir"), getClickHouseContainerTmpDir(),
176177
BindMode.READ_WRITE)
177178
.withNetwork(network)

clickhouse-client/src/test/resources/empty.csv

Whitespace-only changes.

client-v2/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
<artifactId>jackson-core</artifactId>
6767
<version>2.17.2</version>
6868
</dependency>
69+
6970
<!-- Test Dependencies -->
7071
<dependency>
7172
<groupId>com.google.code.gson</groupId>

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
import java.net.NoRouteToHostException;
6464
import java.net.URI;
6565
import java.net.URISyntaxException;
66-
import java.net.URLEncoder;
6766
import java.net.UnknownHostException;
6867
import java.nio.charset.StandardCharsets;
6968
import java.security.NoSuchAlgorithmException;
@@ -288,36 +287,49 @@ public Exception readError(ClassicHttpResponse httpResponse) {
288287

289288
byte [] buffer = new byte[ERROR_BODY_BUFFER_SIZE];
290289
byte [] lookUpStr = String.format(ERROR_CODE_PREFIX_PATTERN, serverCode).getBytes(StandardCharsets.UTF_8);
290+
StringBuilder msgBuilder = new StringBuilder();
291+
boolean found = false;
291292
while (true) {
292293
int rBytes = body.read(buffer);
293294
if (rBytes == -1) {
294295
break;
295-
} else {
296-
for (int i = 0; i < rBytes; i++) {
297-
if (buffer[i] == lookUpStr[0]) {
298-
boolean found = true;
299-
for (int j = 1; j < Math.min(rBytes - i, lookUpStr.length); j++) {
300-
if (buffer[i + j] != lookUpStr[j]) {
301-
found = false;
302-
break;
303-
}
304-
}
305-
if (found) {
306-
int start = i;
307-
while (i < rBytes && buffer[i] != '\n') {
308-
i++;
309-
}
296+
}
310297

311-
return new ServerException(serverCode, new String(buffer, start, i -start, StandardCharsets.UTF_8));
298+
for (int i = 0; i < rBytes; i++) {
299+
if (buffer[i] == lookUpStr[0]) {
300+
found = true;
301+
for (int j = 1; j < Math.min(rBytes - i, lookUpStr.length); j++) {
302+
if (buffer[i + j] != lookUpStr[j]) {
303+
found = false;
304+
break;
312305
}
313306
}
307+
if (found) {
308+
msgBuilder.append(new String(buffer, i, rBytes - i, StandardCharsets.UTF_8));
309+
break;
310+
}
314311
}
315312
}
313+
314+
if (found) {
315+
break;
316+
}
316317
}
317318

319+
while (true) {
320+
int rBytes = body.read(buffer);
321+
if (rBytes == -1) {
322+
break;
323+
}
324+
msgBuilder.append(new String(buffer, 0, rBytes, StandardCharsets.UTF_8));
325+
}
326+
327+
String msg = msgBuilder.toString().replaceAll("\\s+", " ").replaceAll("\\\\n", " ")
328+
.replaceAll("\\\\/", "/");
329+
return new ServerException(serverCode, msg);
330+
} catch (Exception e) {
331+
LOG.error("Failed to read error message", e);
318332
return new ServerException(serverCode, String.format(ERROR_CODE_PREFIX_PATTERN, serverCode) + " <Unreadable error message>");
319-
} catch (IOException e) {
320-
throw new ClientException("Failed to read response body", e);
321333
}
322334
}
323335

@@ -504,26 +516,28 @@ private void addQueryParams(URIBuilder req, Map<String, String> chConfig, Map<St
504516
}
505517

506518
private HttpEntity wrapEntity(HttpEntity httpEntity, int httpStatus, boolean isResponse) {
507-
508-
switch (httpStatus) {
509-
case HttpStatus.SC_OK:
510-
case HttpStatus.SC_CREATED:
511-
case HttpStatus.SC_ACCEPTED:
512-
case HttpStatus.SC_NO_CONTENT:
513-
case HttpStatus.SC_PARTIAL_CONTENT:
514-
case HttpStatus.SC_RESET_CONTENT:
515-
case HttpStatus.SC_NOT_MODIFIED:
516-
case HttpStatus.SC_BAD_REQUEST:
517-
boolean serverCompression = chConfiguration.getOrDefault(ClickHouseClientOption.COMPRESS.getKey(), "false").equalsIgnoreCase("true");
518-
boolean clientCompression = chConfiguration.getOrDefault(ClickHouseClientOption.DECOMPRESS.getKey(), "false").equalsIgnoreCase("true");
519-
boolean useHttpCompression = chConfiguration.getOrDefault("client.use_http_compression", "false").equalsIgnoreCase("true");
520-
if (serverCompression || clientCompression) {
519+
boolean serverCompression = MapUtils.getFlag(chConfiguration, ClickHouseClientOption.COMPRESS.getKey(), false);
520+
boolean clientCompression = MapUtils.getFlag(chConfiguration, ClickHouseClientOption.DECOMPRESS.getKey(), false);
521+
522+
if (serverCompression || clientCompression) {
523+
// Server doesn't compress certain errors like 403
524+
switch (httpStatus) {
525+
case HttpStatus.SC_OK:
526+
case HttpStatus.SC_CREATED:
527+
case HttpStatus.SC_ACCEPTED:
528+
case HttpStatus.SC_NO_CONTENT:
529+
case HttpStatus.SC_PARTIAL_CONTENT:
530+
case HttpStatus.SC_RESET_CONTENT:
531+
case HttpStatus.SC_NOT_MODIFIED:
532+
case HttpStatus.SC_BAD_REQUEST:
533+
case HttpStatus.SC_INTERNAL_SERVER_ERROR:
534+
boolean useHttpCompression = MapUtils.getFlag(chConfiguration, "client.use_http_compression", false);
521535
return new LZ4Entity(httpEntity, useHttpCompression, serverCompression, clientCompression,
522536
MapUtils.getInt(chConfiguration, "compression.lz4.uncompressed_buffer_size"), isResponse);
523-
}
524-
default:
525-
return httpEntity;
537+
}
526538
}
539+
540+
return httpEntity;
527541
}
528542

529543
public static int getHeaderInt(Header header, int defaultValue) {

client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.time.temporal.ChronoUnit;
3939
import java.util.Arrays;
4040
import java.util.Base64;
41+
import java.util.EnumSet;
4142
import java.util.List;
4243
import java.util.Random;
4344
import java.util.concurrent.CompletableFuture;
@@ -308,15 +309,14 @@ public static Object[][] noResponseFailureProvider() {
308309
}
309310

310311
@Test(groups = { "integration" }, dataProvider = "testServerErrorHandlingDataProvider")
311-
public void testServerErrorHandling(ClickHouseFormat format) {
312+
public void testServerErrorHandling(ClickHouseFormat format, boolean serverCompression, boolean useHttpCompression) {
312313
ClickHouseNode server = getServer(ClickHouseProtocol.HTTP);
313314
try (Client client = new Client.Builder()
314315
.addEndpoint(server.getBaseUri())
315316
.setUsername("default")
316317
.setPassword("")
317-
.useNewImplementation(true)
318-
// TODO: fix in old client
319-
// .useNewImplementation(System.getProperty("client.tests.useNewImplementation", "false").equals("true"))
318+
.compressServerResponse(serverCompression)
319+
.useHttpCompression(useHttpCompression)
320320
.build()) {
321321

322322
QuerySettings querySettings = new QuerySettings().setFormat(format);
@@ -329,17 +329,62 @@ public void testServerErrorHandling(ClickHouseFormat format) {
329329
Assert.assertTrue(e.getMessage().startsWith("Code: 62. DB::Exception: Syntax error (Multi-statements are not allowed): failed at position 15 (end of query)"),
330330
"Unexpected error message: " + e.getMessage());
331331
}
332+
333+
334+
try (QueryResponse response = client.query("CREATE TABLE table_from_csv AS SELECT * FROM file('empty.csv')", querySettings)
335+
.get(1, TimeUnit.SECONDS)) {
336+
Assert.fail("Expected exception");
337+
} catch (ServerException e) {
338+
e.printStackTrace();
339+
Assert.assertEquals(e.getCode(), 636);
340+
Assert.assertTrue(e.getMessage().startsWith("Code: 636. DB::Exception: The table structure cannot be extracted from a CSV format file. Error: The table structure cannot be extracted from a CSV format file: the file is empty. You can specify the structure manually: (in file/uri /var/lib/clickhouse/user_files/empty.csv). (CANNOT_EXTRACT_TABLE_STRUCTURE)"),
341+
"Unexpected error message: " + e.getMessage());
342+
}
343+
344+
332345
} catch (Exception e) {
333346
e.printStackTrace();
334347
Assert.fail(e.getMessage(), e);
335348
}
349+
350+
try (Client client = new Client.Builder()
351+
.addEndpoint(server.getBaseUri())
352+
.setUsername("non-existing-user")
353+
.setPassword("nothing")
354+
.compressServerResponse(serverCompression)
355+
.useHttpCompression(useHttpCompression)
356+
.build()) {
357+
358+
try (QueryResponse response = client.query("SELECT 1").get(1, TimeUnit.SECONDS)) {
359+
Assert.fail("Expected exception");
360+
} catch (ServerException e) {
361+
e.printStackTrace();
362+
Assert.assertEquals(e.getCode(), 516);
363+
Assert.assertTrue(e.getMessage().startsWith("Code: 516. DB::Exception: non-existing-user: Authentication failed: password is incorrect, or there is no user with such name. (AUTHENTICATION_FAILED)"),
364+
e.getMessage());
365+
} catch (Exception e) {
366+
e.printStackTrace();
367+
Assert.fail("Unexpected exception", e);
368+
}
369+
}
336370
}
337371

338372
@DataProvider(name = "testServerErrorHandlingDataProvider")
339-
public static Object[] testServerErrorHandlingDataProvider() {
340-
return new Object[] { ClickHouseFormat.JSON, ClickHouseFormat.TabSeparated, ClickHouseFormat.RowBinary };
341-
}
373+
public static Object[][] testServerErrorHandlingDataProvider() {
374+
EnumSet<ClickHouseFormat> formats = EnumSet.of(ClickHouseFormat.CSV, ClickHouseFormat.TSV,
375+
ClickHouseFormat.JSON, ClickHouseFormat.JSONCompact);
342376

377+
Object[][] result = new Object[formats.size() * 3][];
378+
379+
int i = 0;
380+
for (ClickHouseFormat format : formats) {
381+
result[i++] = new Object[]{format, false, false};
382+
result[i++] = new Object[]{format, true, false};
383+
result[i++] = new Object[]{format, true, true};
384+
}
385+
386+
return result;
387+
}
343388

344389
@Test(groups = { "integration" })
345390
public void testErrorWithSuccessfulResponse() {
@@ -377,6 +422,61 @@ public void testErrorWithSuccessfulResponse() {
377422
}
378423
}
379424

425+
@Test(groups = { "integration" }, dataProvider = "testServerErrorsUncompressedDataProvider")
426+
public void testServerErrorsUncompressed(int code, String message, String expectedMessage) {
427+
WireMockServer mockServer = new WireMockServer( WireMockConfiguration
428+
.options().port(9090).notifier(new ConsoleNotifier(false)));
429+
mockServer.start();
430+
431+
mockServer.addStubMapping(WireMock.post(WireMock.anyUrl())
432+
.willReturn(WireMock.aResponse()
433+
.withStatus(HttpStatus.SC_OK)
434+
.withChunkedDribbleDelay(2, 200)
435+
.withHeader("X-ClickHouse-Exception-Code", String.valueOf(code))
436+
.withHeader("X-ClickHouse-Summary",
437+
"{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")
438+
.withBody(message))
439+
.build());
440+
441+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false)
442+
.setUsername("default")
443+
.setPassword("")
444+
.compressServerResponse(false)
445+
.build()) {
446+
447+
try (QueryResponse response = client.query("SELECT 1").get(1, TimeUnit.SECONDS)) {
448+
Assert.fail("Expected exception");
449+
} catch (ServerException e) {
450+
e.printStackTrace();
451+
Assert.assertEquals(e.getCode(), code);
452+
Assert.assertEquals(e.getMessage(), expectedMessage);
453+
} catch (Exception e) {
454+
e.printStackTrace();
455+
Assert.fail("Unexpected exception", e);
456+
}
457+
} finally {
458+
mockServer.stop();
459+
}
460+
}
461+
462+
@DataProvider(name = "testServerErrorsUncompressedDataProvider")
463+
public static Object[][] testServerErrorsUncompressedDataProvider() {
464+
return new Object[][] {
465+
{ 241, "Code: 241. DB::Exception: Memory limit (for query) exceeded: would use 97.21 MiB",
466+
"Code: 241. DB::Exception: Memory limit (for query) exceeded: would use 97.21 MiB"},
467+
{900, "Code: 900. DB::Exception: \uD83D\uDCBE Floppy disk is full",
468+
"Code: 900. DB::Exception: \uD83D\uDCBE Floppy disk is full"},
469+
{901, "Code: 901. DB::Exception: I write, erase, rewrite\n" +
470+
"Erase again, and then\n" +
471+
"A poppy blooms\n" +
472+
" (by Katsushika Hokusai)",
473+
"Code: 901. DB::Exception: I write, erase, rewrite " +
474+
"Erase again, and then " +
475+
"A poppy blooms" +
476+
" (by Katsushika Hokusai)"}
477+
};
478+
}
479+
380480
@Test(groups = { "integration" })
381481
public void testAdditionalHeaders() {
382482
WireMockServer mockServer = new WireMockServer( WireMockConfiguration

0 commit comments

Comments
 (0)