From 3505528f42f8b720f81195999df1cba04c01b7cf Mon Sep 17 00:00:00 2001 From: Animesh Sahu Date: Thu, 18 Jun 2026 15:20:59 +0530 Subject: [PATCH] fix: abort connection before draining in HttpResponse.disconnect() HttpResponse.disconnect() previously called ignore() (which drains the remaining response body via ContentLengthInputStream.close()) before calling LowLevelHttpResponse.disconnect() (which aborts the socket). This caused callers that close a stream after reading only a prefix of a large response to block for minutes draining bytes they do not need. Fix: call response.disconnect() first to abort the socket, then call ignore() for cleanup. Any IOException from ignore() on an already-dead socket is expected and swallowed. This is the corrected version of #1315 (reverted in #1427 because the original lacked the try-catch around ignore(), causing TruncatedChunkException to propagate from disconnect()). Fixes #1303 --- .../apache/v2/ApacheHttpTransportTest.java | 34 +++++++++++++++++++ .../google/api/client/http/HttpResponse.java | 11 +++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/google-http-client-apache-v2/src/test/java/com/google/api/client/http/apache/v2/ApacheHttpTransportTest.java b/google-http-client-apache-v2/src/test/java/com/google/api/client/http/apache/v2/ApacheHttpTransportTest.java index 48a9d1c56..b01cfc47e 100644 --- a/google-http-client-apache-v2/src/test/java/com/google/api/client/http/apache/v2/ApacheHttpTransportTest.java +++ b/google-http-client-apache-v2/src/test/java/com/google/api/client/http/apache/v2/ApacheHttpTransportTest.java @@ -338,4 +338,38 @@ public void handle(HttpExchange httpExchange) throws IOException { private boolean isWindows() { return System.getProperty("os.name").startsWith("Windows"); } + + @Test(timeout = 10_000L) + public void testDisconnectShouldNotWaitToReadResponse() throws IOException { + // This handler waits for 100s before returning writing content. The test should + // timeout if disconnect waits for the response before closing the connection. + final HttpHandler handler = + new HttpHandler() { + @Override + public void handle(HttpExchange httpExchange) throws IOException { + byte[] response = httpExchange.getRequestURI().toString().getBytes(); + httpExchange.sendResponseHeaders(200, response.length); + + // Sleep for longer than the test timeout + try { + Thread.sleep(100_000); + } catch (InterruptedException e) { + throw new IOException("interrupted", e); + } + try (OutputStream out = httpExchange.getResponseBody()) { + out.write(response); + } + } + }; + + try (FakeServer server = new FakeServer(handler)) { + HttpTransport transport = new ApacheHttpTransport(); + GenericUrl testUrl = new GenericUrl("http://localhost/foo//bar"); + testUrl.setPort(server.getPort()); + com.google.api.client.http.HttpResponse response = + transport.createRequestFactory().buildGetRequest(testUrl).execute(); + // disconnect should not wait to read the entire content + response.disconnect(); + } + } } diff --git a/google-http-client/src/main/java/com/google/api/client/http/HttpResponse.java b/google-http-client/src/main/java/com/google/api/client/http/HttpResponse.java index 130208671..7f75bccd6 100644 --- a/google-http-client/src/main/java/com/google/api/client/http/HttpResponse.java +++ b/google-http-client/src/main/java/com/google/api/client/http/HttpResponse.java @@ -438,11 +438,20 @@ public void ignore() throws IOException { * Disconnect using {@link LowLevelHttpResponse#disconnect()}, then close the HTTP response * content using {@link #ignore}. * + *

The underlying connection is aborted first so that closing the response content in {@link + * #ignore} does not block to drain remaining response bytes (e.g. when only a small prefix of a + * large response was read). Any {@link IOException} from {@link #ignore} after an abort is + * expected and silently swallowed. + * * @since 1.4 */ public void disconnect() throws IOException { - ignore(); response.disconnect(); + try { + ignore(); + } catch (IOException e) { + // Expected when the connection was aborted before content was fully consumed. + } } /**