This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-vfs.git
The following commit(s) were added to refs/heads/master by this push:
new 102d4621e Fix UrlFileObject when Java 8's KeepAliveStream reuse causes
(#780)
102d4621e is described below
commit 102d4621e19e1cde16db239a1f0c4d4d6804978d
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Aug 23 18:59:16 2026 -0400
Fix UrlFileObject when Java 8's KeepAliveStream reuse causes (#780)
IOException: Stream closed.
doGetContentSize, doGetLastModifiedTime and doGetType now use HEAD for
HttpURLConnection and call conn.connect() instead of opening and closing
a full GET InputStream. This removes the body download and avoids the
Java 8 KeepAliveStream reuse that causes IOException: Stream closed.
doGetInputStream now sets Connection: close for HTTP URLs on Java 8 so
the pooled socket is not reused for the next metadata call.
With this change LastModifiedTests.testGetLastModifiedFile and the
VfsClassLoaderTests and UrlProviderHttpTest provider tests no longer hit
the Stream closed error on Java 8 on macOS and Windows.
---
.../commons/vfs2/provider/url/UrlFileObject.java | 42 ++++---
.../provider/url/UrlFileObjectKeepAliveTest.java | 134 +++++++++++++++++++++
src/changes/changes.xml | 1 +
3 files changed, 162 insertions(+), 15 deletions(-)
diff --git
a/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/url/UrlFileObject.java
b/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/url/UrlFileObject.java
index 2d1cef5c0..58c9073eb 100644
---
a/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/url/UrlFileObject.java
+++
b/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/url/UrlFileObject.java
@@ -62,7 +62,6 @@ public class UrlFileObject extends
AbstractFileObject<UrlFileSystem> {
protected URL createURL(final FileName name) throws IOException {
if (name instanceof URLFileName) {
final URLFileName urlName = (URLFileName) getName();
-
// TODO: charset
return new URL(urlName.getURIEncoded(null));
}
@@ -87,9 +86,12 @@ public class UrlFileObject extends
AbstractFileObject<UrlFileSystem> {
@Override
protected long doGetContentSize() throws Exception {
final URLConnection conn = url.openConnection();
- try (InputStream unused = conn.getInputStream()) {
- return conn.getContentLength();
+ // Use HEAD for HTTP to avoid downloading body and to avoid Java 8
KeepAliveStream issues
+ if (conn instanceof HttpURLConnection) {
+ ((HttpURLConnection) conn).setRequestMethod("HEAD");
}
+ conn.connect();
+ return conn.getContentLengthLong();
}
/**
@@ -97,7 +99,12 @@ public class UrlFileObject extends
AbstractFileObject<UrlFileSystem> {
*/
@Override
protected InputStream doGetInputStream(final int bufferSize) throws
Exception {
- return url.openStream();
+ final URLConnection conn = url.openConnection();
+ if (conn instanceof HttpURLConnection) {
+ // Disable keep-alive for Java 8 to avoid Stream closed issues on
subsequent metadata calls
+ conn.setRequestProperty("Connection", "close");
+ }
+ return conn.getInputStream();
}
/**
@@ -106,9 +113,11 @@ public class UrlFileObject extends
AbstractFileObject<UrlFileSystem> {
@Override
protected long doGetLastModifiedTime() throws Exception {
final URLConnection conn = url.openConnection();
- try (InputStream unused = conn.getInputStream()) {
- return conn.getLastModified();
+ if (conn instanceof HttpURLConnection) {
+ ((HttpURLConnection) conn).setRequestMethod("HEAD");
}
+ conn.connect();
+ return conn.getLastModified();
}
/**
@@ -119,17 +128,20 @@ public class UrlFileObject extends
AbstractFileObject<UrlFileSystem> {
try {
// Attempt to connect & check status
final URLConnection conn = url.openConnection();
- try (InputStream unused = conn.getInputStream()) {
- if (conn instanceof HttpURLConnection) {
- final int status = ((HttpURLConnection)
conn).getResponseCode();
- // 200 is good, maybe add more later...
- if (HttpURLConnection.HTTP_OK != status) {
- return FileType.IMAGINARY;
- }
+ if (conn instanceof HttpURLConnection) {
+ ((HttpURLConnection) conn).setRequestMethod("HEAD");
+ // Disable keep-alive for Java 8 to avoid Stream closed issues
+ conn.setRequestProperty("Connection", "close");
+ }
+ conn.connect();
+ if (conn instanceof HttpURLConnection) {
+ final int status = ((HttpURLConnection)
conn).getResponseCode();
+ // 200 is good, maybe add more later...
+ if (HttpURLConnection.HTTP_OK != status) {
+ return FileType.IMAGINARY;
}
-
- return FileType.FILE;
}
+ return FileType.FILE;
} catch (final FileNotFoundException e) {
return FileType.IMAGINARY;
}
diff --git
a/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/url/UrlFileObjectKeepAliveTest.java
b/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/url/UrlFileObjectKeepAliveTest.java
new file mode 100644
index 000000000..7cc8cda2f
--- /dev/null
+++
b/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/url/UrlFileObjectKeepAliveTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.vfs2.provider.url;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.provider.AbstractFileName;
+import org.junit.jupiter.api.Test;
+
+public class UrlFileObjectKeepAliveTest {
+
+ private UrlFileObject createObject(final URL url) throws Exception {
+ final FileName rootName = mock(FileName.class);
+ when(rootName.getURI()).thenReturn("http://example.com/");
+ // real FS -> useCount is initialized
+ final UrlFileSystem fs = new UrlFileSystem(rootName, null);
+ final AbstractFileName name = mock(AbstractFileName.class);
+ final UrlFileObject obj = new UrlFileObject(fs, name);
+ final Field urlField = UrlFileObject.class.getDeclaredField("url");
+ urlField.setAccessible(true);
+ urlField.set(obj, url);
+ return obj;
+ }
+
+ @Test
+ public void doGetContentSize_usesHead_andDoesNotOpenStream() throws
Exception {
+ final HttpURLConnection conn = mock(HttpURLConnection.class);
+ when(conn.getContentLengthLong()).thenReturn(1024L);
+ final UrlFileObject obj = createObject(urlWithHandler(conn));
+ final Method m =
UrlFileObject.class.getDeclaredMethod("doGetContentSize");
+ m.setAccessible(true);
+ final long size = (Long) m.invoke(obj);
+ verify(conn).setRequestMethod("HEAD");
+ verify(conn).connect();
+ verify(conn, never()).getInputStream();
+ assertEquals(1024L, size);
+ }
+
+ @Test
+ public void doGetInputStream_setsConnectionCloseForHttp() throws Exception
{
+ final HttpURLConnection conn = mock(HttpURLConnection.class);
+
when(conn.getInputStream()).thenReturn(mock(java.io.InputStream.class));
+ final UrlFileObject obj = createObject(urlWithHandler(conn));
+ final Method m =
UrlFileObject.class.getDeclaredMethod("doGetInputStream", int.class);
+ m.setAccessible(true);
+ m.invoke(obj, 8192);
+ verify(conn).setRequestProperty("Connection", "close");
+ verify(conn).getInputStream();
+ }
+
+ @Test
+ public void doGetLastModifiedTime_nonHttpDoesNotSetHead() throws Exception
{
+ final URLConnection conn = mock(URLConnection.class);
+ when(conn.getLastModified()).thenReturn(999L);
+ final UrlFileObject obj = createObject(urlWithHandler(conn));
+ final Method m =
UrlFileObject.class.getDeclaredMethod("doGetLastModifiedTime");
+ m.setAccessible(true);
+ final long lm = (Long) m.invoke(obj);
+ verify(conn).connect();
+ assertEquals(999L, lm);
+ }
+
+ @Test
+ public void doGetLastModifiedTime_usesHead_andDoesNotOpenStream() throws
Exception {
+ final HttpURLConnection conn = mock(HttpURLConnection.class);
+ when(conn.getLastModified()).thenReturn(123456789L);
+ final UrlFileObject obj = createObject(urlWithHandler(conn));
+ final Method m =
UrlFileObject.class.getDeclaredMethod("doGetLastModifiedTime");
+ m.setAccessible(true);
+ final long lm = (Long) m.invoke(obj);
+ verify(conn).setRequestMethod("HEAD");
+ verify(conn).connect();
+ verify(conn, never()).getInputStream();
+ assertEquals(123456789L, lm);
+ }
+
+ @Test
+ public void doGetType_usesHead_andDoesNotOpenStream() throws Exception {
+ final HttpURLConnection conn = mock(HttpURLConnection.class);
+ when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
+ final UrlFileObject obj = createObject(urlWithHandler(conn));
+ final Method m = UrlFileObject.class.getDeclaredMethod("doGetType");
+ m.setAccessible(true);
+ final Object type = m.invoke(obj);
+ verify(conn).setRequestMethod("HEAD");
+ verify(conn).setRequestProperty("Connection", "close");
+ verify(conn).connect();
+ verify(conn, never()).getInputStream();
+ assertNotNull(type);
+ }
+
+ private URL urlWithHandler(final URLConnection conn) {
+ try {
+ return new URL(null, "http://example.com/path", new
URLStreamHandler() {
+
+ @Override
+ protected URLConnection openConnection(final URL u) throws
IOException {
+ return conn;
+ }
+ });
+ } catch (final Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 97f842354..08a1777df 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -72,6 +72,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="fix" dev="ggregory" due-to="Naveed Khan, Gary
Gregory">Reject non-ascii hex digits in UriParser percent-decoding
(#774).</action>
<action type="fix" dev="ggregory" due-to="Naveed Khan, Gary
Gregory">Replace the shared vfs_cache temp dir in DefaultFileReplicator
(#775).</action>
<action type="fix" dev="ggregory" due-to="Naveed Khan, Gary Gregory">Fix
FtpProviderUserDirTest on Java 8 on Ubuntu (#779).</action>
+ <action type="fix" dev="ggregory" due-to="Naveed Khan, Gary Gregory">Fix
UrlFileObject when Java 8's KeepAliveStream reuse causes IOException: Stream
closed.</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary Gregory">Add
org.apache.commons.vfs2.provider.ftp.FTPClientWrapper.sendOptions(String,
String).</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add
FtpFileSystemConfigBuilder.getControlEncodingCharset(FileSystemOptions) and
deprecate getControlEncoding(FileSystemOptions).</action>