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 06813177f Fix HTTP random reads when range requests are ignored
06813177f is described below
commit 06813177f9e6783e26352c84afc2e2d7d8f8a8d6
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Oct 30 10:38:01 2026 -0400
Fix HTTP random reads when range requests are ignored
Use IOUtils.skip() to handle short skips in the legacy HTTP provider.
Release the response stream if positioning fails, preserving the
original
exception and suppressing distinct cleanup exceptions.
Add regression tests for short skips, EOF, and cleanup failures.
---
.../provider/http/HttpRandomAccessContent.java | 22 ++-
.../provider/http/HttpRandomAccessContentTest.java | 161 +++++++++++++++++++++
src/changes/changes.xml | 1 +
3 files changed, 180 insertions(+), 4 deletions(-)
diff --git
a/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/http/HttpRandomAccessContent.java
b/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/http/HttpRandomAccessContent.java
index 5d31fbcc1..e5ca47d9f 100644
---
a/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/http/HttpRandomAccessContent.java
+++
b/commons-vfs2/src/main/java/org/apache/commons/vfs2/provider/http/HttpRandomAccessContent.java
@@ -22,6 +22,7 @@ import java.io.IOException;
import java.net.HttpURLConnection;
import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.provider.AbstractRandomAccessStreamContent;
import org.apache.commons.vfs2.util.MonitorInputStream;
@@ -81,10 +82,23 @@ final class HttpRandomAccessContent<FS extends
HttpFileSystem> extends AbstractR
monitorInputStream = new HttpFileObject.HttpInputStream(getMethod);
// If the range request was ignored
if (status == HttpURLConnection.HTTP_OK) {
- final long skipped = monitorInputStream.skip(filePointer);
- if (skipped != filePointer) {
- throw new
FileSystemException("vfs.provider.http/get-range.error", fileObject.getName(),
- Long.valueOf(filePointer), Integer.valueOf(status));
+ try {
+ final long skipped = IOUtils.skip(monitorInputStream,
filePointer);
+ if (skipped != filePointer) {
+ throw new
FileSystemException("vfs.provider.http/get-range.error", fileObject.getName(),
+ Long.valueOf(filePointer),
Integer.valueOf(status));
+ }
+ } catch (final IOException e) {
+ try {
+ IOUtils.closeQuietly(monitorInputStream, closeException ->
{
+ if (closeException != e) {
+ e.addSuppressed(closeException);
+ }
+ });
+ } finally {
+ monitorInputStream = null;
+ }
+ throw e;
}
}
dataInputStream = new DataInputStream(new
FilterInputStream(monitorInputStream) {
diff --git
a/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/http/HttpRandomAccessContentTest.java
b/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/http/HttpRandomAccessContentTest.java
new file mode 100644
index 000000000..41ff36a54
--- /dev/null
+++
b/commons-vfs2/src/test/java/org/apache/commons/vfs2/provider/http/HttpRandomAccessContentTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.http;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpConnection;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpState;
+import org.apache.commons.httpclient.params.HttpConnectionParams;
+import org.apache.commons.httpclient.protocol.Protocol;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.util.RandomAccessMode;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests random reads when the HTTP server ignores range requests.
+ */
+class HttpRandomAccessContentTest {
+
+ private HttpRandomAccessContent<HttpFileSystem> createContent(final
HttpConnection connection) throws Exception {
+ return createContent(connection, null, null);
+ }
+
+ private HttpRandomAccessContent<HttpFileSystem> createContent(final
HttpConnection connection, final IOException readFailure,
+ final IOException closeFailure) throws Exception {
+ final byte[] headers = "Content-Length:
32\r\n\r\n".getBytes(StandardCharsets.US_ASCII);
+ final byte[] response = new byte[headers.length + 32];
+ System.arraycopy(headers, 0, response, 0, headers.length);
+ for (int i = 0; i < 32; i++) {
+ response[headers.length + i] = (byte) i;
+ }
+ // Parsing the headers leaves seven body bytes in the connection
buffer.
+ final FilterInputStream source = new FilterInputStream(new
ByteArrayInputStream(response)) {
+
+ private int readCount;
+
+ @Override
+ public int read(final byte[] bytes, final int offset, final int
length) throws IOException {
+ final int currentRead = readCount++;
+ if (currentRead == 1 && readFailure != null) {
+ throw readFailure;
+ }
+ if (currentRead == 2 && closeFailure != null) {
+ throw closeFailure;
+ }
+ return super.read(bytes, offset, length);
+ }
+ };
+ final BufferedInputStream input = new BufferedInputStream(source,
headers.length + 7);
+ when(connection.isOpen()).thenReturn(true);
+ when(connection.getHost()).thenReturn("localhost");
+ when(connection.getPort()).thenReturn(80);
+
when(connection.getProtocol()).thenReturn(Protocol.getProtocol("http"));
+ when(connection.getParams()).thenReturn(new HttpConnectionParams());
+ when(connection.getRequestOutputStream()).thenReturn(new
ByteArrayOutputStream());
+ when(connection.readLine(anyString())).thenReturn("HTTP/1.1 200 OK");
+ when(connection.getResponseInputStream()).thenReturn(input);
+ final HttpClient client = mock(HttpClient.class);
+ when(client.executeMethod(any(HttpMethod.class)))
+ .thenAnswer(invocation -> ((HttpMethod)
invocation.getArgument(0)).execute(new HttpState(), connection));
+ final HttpFileSystem fileSystem = mock(HttpFileSystem.class);
+ when(fileSystem.getClient()).thenReturn(client);
+ @SuppressWarnings("unchecked")
+ final HttpFileObject<HttpFileSystem> file = mock(HttpFileObject.class);
+ when(file.getFileSystem()).thenReturn(fileSystem);
+ return new HttpRandomAccessContent<>(file, RandomAccessMode.READ);
+ }
+
+ @Test
+ void testIgnoredRangeBeyondEndClosesConnection() throws Exception {
+ final HttpConnection connection = mock(HttpConnection.class);
+ try (HttpRandomAccessContent<HttpFileSystem> content =
createContent(connection)) {
+ content.seek(40);
+ assertThrows(FileSystemException.class, content::readByte);
+ assertEquals(40, content.getFilePointer());
+ verify(connection).releaseConnection();
+ }
+ }
+
+ @Test
+ void testIgnoredRangeCloseFailureIsSuppressed() throws Exception {
+ final HttpConnection connection = mock(HttpConnection.class);
+ final IOException readFailure = new IOException("Read failed");
+ final IOException closeFailure = new IOException("Close failed");
+ try (HttpRandomAccessContent<HttpFileSystem> content =
createContent(connection, readFailure, closeFailure)) {
+ content.seek(10);
+ assertSame(readFailure, assertThrows(IOException.class,
content::readByte));
+ assertEquals(1, readFailure.getSuppressed().length);
+ assertSame(closeFailure, readFailure.getSuppressed()[0]);
+ verify(connection).releaseConnection();
+ }
+ }
+
+ @Test
+ void testIgnoredRangeReadFailureClosesConnection() throws Exception {
+ final HttpConnection connection = mock(HttpConnection.class);
+ final IOException failure = new IOException("Read failed");
+ try (HttpRandomAccessContent<HttpFileSystem> content =
createContent(connection, failure, null)) {
+ content.seek(10);
+ assertSame(failure, assertThrows(IOException.class,
content::readByte));
+ assertEquals(10, content.getFilePointer());
+ verify(connection).releaseConnection();
+ }
+ }
+
+ @Test
+ void testIgnoredRangeSameReadAndCloseFailure() throws Exception {
+ final HttpConnection connection = mock(HttpConnection.class);
+ final IOException failure = new IOException("Read and close failed");
+ try (HttpRandomAccessContent<HttpFileSystem> content =
createContent(connection, failure, failure)) {
+ content.seek(10);
+ assertSame(failure, assertThrows(IOException.class,
content::readByte));
+ assertEquals(0, failure.getSuppressed().length);
+ verify(connection).releaseConnection();
+ }
+ }
+
+ @Test
+ void testIgnoredRangeWithShortSkip() throws Exception {
+ final HttpConnection connection = mock(HttpConnection.class);
+ try (HttpRandomAccessContent<HttpFileSystem> content =
createContent(connection)) {
+ content.seek(10);
+ assertEquals(10, content.readByte());
+ assertEquals(11, content.getFilePointer());
+ assertEquals(11, content.readByte());
+ assertEquals(12, content.getFilePointer());
+ }
+ verify(connection).releaseConnection();
+ }
+}
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 4d84ea89d..7bc7cfc08 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -74,6 +74,7 @@ The <action> type attribute can be add,update,fix,remove.
<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>
+ <action type="fix" dev="ggregory" due-to="Gary Gregory">Fix HTTP random
reads when range requests are ignored.</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>