diff --git a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java index e5892748d..67e1d591d 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReader.java @@ -1086,7 +1086,36 @@ public ClickHouseBitmap getClickHouseBitmap(int index) { @Override public void close() throws Exception { - input.close(); + try { + input.close(); + } catch (Exception e) { + // Apache HttpComponents ChunkedInputStream.close() drains remaining bytes and + // throws ConnectionClosedException ("Premature end of chunk coded message body") + // when the server tore the connection down mid-response (e.g. send_timeout fired + // before the terminating zero-length chunk was written). Any real iteration-time + // failure has already surfaced through nextRecord(); a drain failure at close() + // is informational only and should not punish callers of try-with-resources. + if (isConnectionClosedException(e)) { + LOG.debug("Swallowing chunked-stream drain on reader close: {}", e.toString()); + return; + } + throw e; + } + } + + /** + * Walks the cause chain looking for an {@code org.apache.hc.core5.http.ConnectionClosedException}. + * Matched by class-name suffix so it works against both directly-referenced and shaded copies of + * the HC class without taking a compile-time dependency on it from this layer. + */ + static boolean isConnectionClosedException(Throwable t) { + while (t != null) { + if (t.getClass().getName().endsWith(".ConnectionClosedException")) { + return true; + } + t = t.getCause(); + } + return false; } private static class RecordWrapper implements Map { diff --git a/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReaderCloseTest.java b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReaderCloseTest.java new file mode 100644 index 000000000..81eefa676 --- /dev/null +++ b/client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/AbstractBinaryFormatReaderCloseTest.java @@ -0,0 +1,36 @@ +package com.clickhouse.client.api.data_formats.internal; + +import org.apache.hc.core5.http.ConnectionClosedException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; + +import static org.testng.Assert.assertEquals; + +/** + * Verifies that the reader's close path recognises Apache HC's + * ConnectionClosedException (the "Premature end of chunk coded message body" + * surface that fires when the server tore the connection down before writing + * the terminating zero-length chunk) and is willing to swallow it on close. + * Matched by class-name suffix so the recogniser works against both the + * directly-referenced and the shaded copy of the HC class. + */ +@Test(groups = {"unit"}) +public class AbstractBinaryFormatReaderCloseTest { + + @DataProvider(name = "cases") + public Object[][] cases() { + return new Object[][] { + { new ConnectionClosedException("Premature end of chunk coded message body: closing chunk expected"), true }, + { new IOException("close failed", new ConnectionClosedException("closing chunk expected")), true }, + { new IOException("disk full"), false }, + { null, false }, + }; + } + + @Test(dataProvider = "cases") + public void recognisesHcConnectionClosed(Throwable t, boolean expected) { + assertEquals(AbstractBinaryFormatReader.isConnectionClosedException(t), expected); + } +}