Skip to content

Storage - SeekableByteChannel Unnecessary Request Bugfix#49526

Open
ibrandes wants to merge 9 commits into
Azure:mainfrom
ibrandes:storage/byteChannelReadBugfix
Open

Storage - SeekableByteChannel Unnecessary Request Bugfix#49526
ibrandes wants to merge 9 commits into
Azure:mainfrom
ibrandes:storage/byteChannelReadBugfix

Conversation

@ibrandes

@ibrandes ibrandes commented Jun 17, 2026

Copy link
Copy Markdown
Member

Fix unnecessary HTTP 416 round trip in openSeekableByteChannelRead

Addresses #38070

Summary

This PR fixes two related issues in StorageSeekableByteChannelBlobReadBehavior (used by BlobClientBase.openSeekableByteChannelRead) that caused an unnecessary HTTP request after a blob's entire content had already been delivered to the caller.

  1. Unnecessary HTTP 416 round trip: When a seekable byte channel is opened with ETag consistency control (the default), the read behavior previously issued an additional downloadStreamWithResponse call even after the full blob had been returned in the initial range download. Because the requested offset was at or past the end of the blob, the service rejected this request with an HTTP 416 (Requested Range Not Satisfiable) response. The read now short-circuits to end-of-file (-1) once the known resource length is reached, avoiding the extra call entirely.

  2. Transport error while streaming the trailing 416 response: Even when the extra request was issued, streaming the body of the 416 response could fail with a transport-level error (for example, "Connection reset by peer" wrapped in reactor's ReactiveException). Since the requested offset was already at or past the known end of the resource, no data could have been returned anyway. The read now logs a warning and signals end-of-file instead of propagating the exception to a caller that has already received all of the blob's content.

Reason for the changes

Callers using openSeekableByteChannelRead were seeing an unnecessary HTTP 416 response—and in some cases an unexpected exception (e.g., connection reset)—after they had already successfully read the entire blob. Made evident by stress testing, this caused confusing errors and wasted a network round trip on what should be a clean end-of-file signal.

Changes

StorageSeekableByteChannelBlobReadBehavior.java

  • Short-circuit to EOF when sourceOffset >= resourceLength and the blob content is locked via an If-Match ETag (the content cannot have grown without invalidating the precondition), via a new isEtagLocked() helper.
  • Added a RuntimeException catch block that, when the requested offset is at or past the known end of the resource, logs a warning and returns -1 instead of propagating the error.
  • Errors that occur while reading within the known bounds of the resource continue to propagate, since requested bytes could not be retrieved.

CHANGELOG.md

  • Added two Bugs Fixed entries describing the fixes.

Behavior preserved

  • When the blob is not ETag-locked, a read past the end still issues a request so the behavior can detect blob growth (existing contract unchanged).
  • Transport errors that occur while reading within the known resource bounds still surface as exceptions.

@github-actions github-actions Bot added the Storage Storage Service (Queues, Blobs, Files) label Jun 17, 2026
@ibrandes ibrandes changed the title Storage - SeekableByteChannel Bugfixes Storage - SeekableByteChannel Unnecessary Request Bugfix Jun 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an inefficiency and related error handling in BlobClientBase.openSeekableByteChannelRead by updating the underlying StorageSeekableByteChannelBlobReadBehavior to avoid an unnecessary trailing HTTP 416 call after the full blob has already been read, and to treat certain transport-level failures at EOF as end-of-file instead of surfacing exceptions to the caller.

Changes:

  • Short-circuit reads at/after the known blob length to EOF when the read is protected by an If-Match ETag (avoids an unnecessary 416 round trip).
  • Swallow and warn on transport-level RuntimeException failures when reading at/after the known EOF, returning -1 instead.
  • Add unit tests for the new behaviors and document the fixes in the Storage Blob changelog.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/specialized/StorageSeekableByteChannelBlobReadBehavior.java Adds EOF short-circuit for ETag-locked reads and runtime-exception handling when reading at/after known EOF.
sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/specialized/StorageSeekableByteChannelBlobReadBehaviorTests.java Adds coverage validating no-call EOF behavior when ETag-locked, and EOF signaling behavior on transport failures at EOF.
sdk/storage/azure-storage-blob/CHANGELOG.md Documents the unnecessary 416 request fix and the transport-error-at-EOF behavior change.

Comment thread sdk/storage/azure-storage-blob/CHANGELOG.md
ibrandes and others added 3 commits June 16, 2026 22:14
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@ibrandes ibrandes marked this pull request as ready for review June 17, 2026 05:22

@browndav-msft browndav-msft left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the PR looks good. I just added something things that might help clarity and perhaps adding a test for blobShrink, if that's a thing.

}

String ifMatch = requestConditions.getIfMatch();
return !CoreUtils.isNullOrEmpty(ifMatch) && !"*".equals(ifMatch.trim());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is just hard to read because of the multiple negations. Is it possible to simplify this so that it is just easier to understand e.g. isIfMatchPopulated(ifMatch) && isIfMatchNotAWildcard(ifMatch)

idk, it just takes way more time to read it with all of the double negations.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe something like this might be easier to understand:

    String ifMatch = requestConditions.getIfMatch();
    if (CoreUtils.isNullOrEmpty(ifMatch)) {
        return false;
    }

    // A specific ETag locks the blob's content; the wildcard "*" matches any blob and does not.
    String trimmedIfMatch = ifMatch.trim();
    return !trimmedIfMatch.isEmpty() && !trimmedIfMatch.equals("*");

*/
@Test
public void readPastEndShortCircuitsWhenETagLocked() throws IOException {
BlobClientBase client = Mockito.mock(BlobClientBase.class);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we could move this to the @BeforeEach setup() and create a mockClient instance variable

* must continue to surface as exceptions, since some bytes the caller asked for could not be retrieved.
*/
@Test
public void readWithinResourcePropagatesTransportError() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a clearer name: readBeforeEndOfBlobPropagatesTransportError

}

@Test
void readDetectsBlobGrowth() throws IOException {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be a readDetectsBlobShrink(). I don't even know if this is possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Storage Storage Service (Queues, Blobs, Files)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Storage blobClient.openSeekableByteChannelRead makes unnecessary request to determine if stream has ended

3 participants