From 81d9e61898ba1b9930b96615ff11d352b60aad83 Mon Sep 17 00:00:00 2001
From: Local Merge
Date: Wed, 20 May 2026 10:51:29 +0530
Subject: [PATCH 01/10] content length override changes
---
.../models/BlobsDownloadHeaders.java | 36 ++++++++++++-
.../blob/models/BlobDownloadHeaders.java | 21 ++++++++
.../common/implementation/Constants.java | 5 ++
.../common/policy/DecodedResponse.java | 26 +++++++---
...StorageContentValidationDecoderPolicy.java | 21 +++++++-
.../common/policy/DecodedResponseTests.java | 52 +++++++++++++------
6 files changed, 137 insertions(+), 24 deletions(-)
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java
index 1d409a5a4cdd..b060c4c88c33 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java
@@ -301,6 +301,11 @@ public final class BlobsDownloadHeaders {
@Generated
private byte[] xMsContentCrc64;
+ /*
+ * The x-ms-original-content-length property.
+ */
+ private Long xMsOriginalContentLength;
+
private static final HttpHeaderName X_MS_CREATION_TIME = HttpHeaderName.fromString("x-ms-creation-time");
private static final HttpHeaderName X_MS_OR_POLICY_ID = HttpHeaderName.fromString("x-ms-or-policy-id");
@@ -369,6 +374,9 @@ public final class BlobsDownloadHeaders {
private static final HttpHeaderName X_MS_CONTENT_CRC64 = HttpHeaderName.fromString("x-ms-content-crc64");
+ private static final HttpHeaderName X_MS_ORIGINAL_CONTENT_LENGTH
+ = HttpHeaderName.fromString("x-ms-original-content-length");
+
// HttpHeaders containing the raw property values.
/**
* Creates an instance of BlobsDownloadHeaders class.
@@ -535,6 +543,12 @@ public BlobsDownloadHeaders(HttpHeaders rawHeaders) {
} else {
this.xMsContentCrc64 = null;
}
+ String xMsOriginalContentLength = rawHeaders.getValue(X_MS_ORIGINAL_CONTENT_LENGTH);
+ if (xMsOriginalContentLength != null) {
+ this.xMsOriginalContentLength = Long.parseLong(xMsOriginalContentLength);
+ } else {
+ this.xMsOriginalContentLength = null;
+ }
Map xMsMetaHeaderCollection = new LinkedHashMap<>();
Map xMsOrHeaderCollection = new LinkedHashMap<>();
@@ -1596,7 +1610,7 @@ public byte[] getXMsContentCrc64() {
/**
* Set the xMsContentCrc64 property: The x-ms-content-crc64 property.
- *
+ *
* @param xMsContentCrc64 the xMsContentCrc64 value to set.
* @return the BlobsDownloadHeaders object itself.
*/
@@ -1605,4 +1619,24 @@ public BlobsDownloadHeaders setXMsContentCrc64(byte[] xMsContentCrc64) {
this.xMsContentCrc64 = CoreUtils.clone(xMsContentCrc64);
return this;
}
+
+ /**
+ * Get the xMsOriginalContentLength property: The wire size of the encoded structured message body before decoding.
+ *
+ * @return the xMsOriginalContentLength value.
+ */
+ public Long getXMsOriginalContentLength() {
+ return this.xMsOriginalContentLength;
+ }
+
+ /**
+ * Set the xMsOriginalContentLength property: The wire size of the encoded structured message body before decoding.
+ *
+ * @param xMsOriginalContentLength the xMsOriginalContentLength value to set.
+ * @return the BlobsDownloadHeaders object itself.
+ */
+ public BlobsDownloadHeaders setXMsOriginalContentLength(Long xMsOriginalContentLength) {
+ this.xMsOriginalContentLength = xMsOriginalContentLength;
+ return this;
+ }
}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
index dfd93a535fc6..812baceb423b 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
@@ -1044,4 +1044,25 @@ public BlobDownloadHeaders setCreationTime(OffsetDateTime creationTime) {
internalHeaders.setXMsCreationTime(creationTime);
return this;
}
+
+ /**
+ * Get the originalContentLength property: The wire size of the encoded structured message body before decoding.
+ * Only present when content validation is active and the response was decoded transparently.
+ *
+ * @return the originalContentLength value.
+ */
+ public Long getOriginalContentLength() {
+ return internalHeaders.getXMsOriginalContentLength();
+ }
+
+ /**
+ * Set the originalContentLength property: The wire size of the encoded structured message body before decoding.
+ *
+ * @param originalContentLength the originalContentLength value to set.
+ * @return the BlobDownloadHeaders object itself.
+ */
+ public BlobDownloadHeaders setOriginalContentLength(Long originalContentLength) {
+ internalHeaders.setXMsOriginalContentLength(originalContentLength);
+ return this;
+ }
}
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java
index c7b4f0198525..fe5bf72e11f7 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java
@@ -264,6 +264,11 @@ public static final class HeaderConstants {
public static final HttpHeaderName STRUCTURED_CONTENT_LENGTH_HEADER_NAME
= HttpHeaderName.fromString(STRUCTURED_CONTENT_LENGTH);
+ public static final String ORIGINAL_CONTENT_LENGTH = "x-ms-original-content-length";
+
+ public static final HttpHeaderName ORIGINAL_CONTENT_LENGTH_HEADER_NAME
+ = HttpHeaderName.fromString(ORIGINAL_CONTENT_LENGTH);
+
/**
* Metadata key ("hdi_isfolder") used to mark virtual directories in Azure Blob Storage.
*
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
index 1f604f0d6cac..e852769598d2 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
@@ -8,6 +8,7 @@
import com.azure.core.http.HttpResponse;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.FluxUtil;
+import com.azure.storage.common.implementation.Constants;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -26,19 +27,30 @@
class DecodedResponse extends HttpResponse {
private final HttpResponse originalResponse;
private final Flux decodedBody;
+ private final HttpHeaders adjustedHeaders;
/**
* Wraps {@code httpResponse} with a body backed by {@code decodedBody}.
*
- * @param httpResponse The original response from the storage service. Its request, status code, and headers
- * are preserved verbatim.
- * @param decodedBody The Flux of CRC-validated, framing-stripped payload bytes produced by the decoder
- * pipeline.
+ * {@code Content-Length} is overridden to {@code decodedContentLength} so callers see the size of the bytes
+ * they will actually read. The original wire size is preserved in {@code x-ms-original-content-length}.
+ *
+ * @param httpResponse The original response from the storage service.
+ * @param decodedBody The Flux of CRC-validated, framing-stripped payload bytes produced by the decoder pipeline.
+ * @param originalContentLength The wire size of the encoded structured message body.
+ * @param decodedContentLength The size of the decoded payload that callers will consume.
*/
- DecodedResponse(HttpResponse httpResponse, Flux decodedBody) {
+ DecodedResponse(HttpResponse httpResponse, Flux decodedBody, long originalContentLength,
+ long decodedContentLength) {
super(httpResponse.getRequest());
this.originalResponse = httpResponse;
this.decodedBody = decodedBody;
+ HttpHeaders headers = new HttpHeaders();
+ httpResponse.getHeaders().stream().forEach(h -> headers.set(h.getName(), h.getValue()));
+ headers.set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(decodedContentLength));
+ headers.set(Constants.HeaderConstants.ORIGINAL_CONTENT_LENGTH_HEADER_NAME,
+ String.valueOf(originalContentLength));
+ this.adjustedHeaders = headers;
}
@Override
@@ -49,12 +61,12 @@ public int getStatusCode() {
@Override
@SuppressWarnings("deprecation")
public String getHeaderValue(String name) {
- return originalResponse.getHeaderValue(name);
+ return adjustedHeaders.getValue(name);
}
@Override
public HttpHeaders getHeaders() {
- return originalResponse.getHeaders();
+ return adjustedHeaders;
}
@Override
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
index 647a95f71e7e..a9017f6a7e05 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
@@ -73,11 +73,14 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN
// Confirm the service actually honored our structured-body request before we hand the body to the decoder.
validateStructuredMessageHeaders(httpResponse);
+ Long decodedContentLength = getStructuredContentLength(httpResponse.getHeaders());
+
// Fresh decoder per response so retries each get a clean state machine.
StructuredMessageDecoder decoder = new StructuredMessageDecoder(contentLength);
Flux decodedStream = decodeStream(httpResponse.getBody(), decoder);
- return new DecodedResponse(httpResponse, decodedStream);
+ // decodedContentLength is guaranteed non-null here: validateStructuredMessageHeaders confirmed its presence.
+ return new DecodedResponse(httpResponse, decodedStream, contentLength, decodedContentLength);
});
}
@@ -123,6 +126,22 @@ private static Long getContentLength(HttpHeaders headers) {
return null;
}
+ /**
+ * Reads {@code x-ms-structured-content-length} as a {@code Long}, returning {@code null} if missing or
+ * unparseable.
+ */
+ private static Long getStructuredContentLength(HttpHeaders headers) {
+ String value = headers.getValue(Constants.HeaderConstants.STRUCTURED_CONTENT_LENGTH_HEADER_NAME);
+ if (value != null) {
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ // Malformed header; fall through to null.
+ }
+ }
+ return null;
+ }
+
/**
* @return true for a 2xx response to a GET request, the only response shape that carries a body we
* can decode. 206 (Partial Content) on retried range downloads is included.
diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java
index 53f34be58f32..82939c292f15 100644
--- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java
+++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java
@@ -10,6 +10,7 @@
import com.azure.core.http.HttpResponse;
import com.azure.core.test.http.MockHttpResponse;
import com.azure.core.util.BinaryData;
+import com.azure.storage.common.implementation.Constants;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
@@ -64,17 +65,19 @@ public void preservesRequestStatusCodeAndHeaders() {
HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "100").set(CUSTOM_HEADER, "value");
MockHttpResponse original = mockResponse(206, h, bytes("encoded"));
- DecodedResponse wrapper = new DecodedResponse(original, fluxOf(bytes("decoded")));
+ DecodedResponse wrapper = new DecodedResponse(original, fluxOf(bytes("decoded")), 100L, 80L);
assertSame(original.getRequest(), wrapper.getRequest());
assertEquals(206, wrapper.getStatusCode());
- assertSame(h, wrapper.getHeaders());
+ // Content-Length is overridden to decoded size; other headers are preserved.
+ assertEquals("80", wrapper.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+ assertEquals("value", wrapper.getHeaders().getValue(CUSTOM_HEADER));
}
@Test
public void getHeaderValueByStringReturnsHeaderValue() {
HttpHeaders h = headers(CUSTOM_HEADER, "value");
- DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]));
+ DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]), 0L, 0L);
assertEquals("value", wrapper.getHeaderValue(CUSTOM_HEADER.getCaseInsensitiveName()));
assertNull(wrapper.getHeaderValue("nonexistent"));
@@ -84,7 +87,7 @@ public void getHeaderValueByStringReturnsHeaderValue() {
public void getBodyReturnsDecodedFlux() {
byte[] decoded = bytes("decoded body");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
StepVerifier.create(wrapper.getBody().reduce(new ByteArrayOutputStream(), (sink, buf) -> {
byte[] copy = new byte[buf.remaining()];
@@ -98,7 +101,7 @@ public void getBodyReturnsDecodedFlux() {
public void getBodyAsByteArrayReturnsDecodedBytes() {
byte[] decoded = bytes("decoded body");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
StepVerifier.create(wrapper.getBodyAsByteArray())
.expectNextMatches(b -> Arrays.equals(decoded, b))
@@ -111,7 +114,7 @@ public void getBodyAsStringDefaultsToUtf8WhenNoCharsetSpecified() {
// BOM nor a Content-Type charset parameter is present. This test pins the "no headers, no BOM" path.
String text = "héllo wörld – ✓";
DecodedResponse wrapper = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]),
- fluxOf(text.getBytes(StandardCharsets.UTF_8)));
+ fluxOf(text.getBytes(StandardCharsets.UTF_8)), 0L, 0L);
StepVerifier.create(wrapper.getBodyAsString()).expectNext(text).verifyComplete();
}
@@ -124,7 +127,7 @@ public void getBodyAsStringHonorsCharsetFromContentTypeHeader() {
String text = "ümlaut";
byte[] iso = text.getBytes(StandardCharsets.ISO_8859_1);
HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, "text/plain; charset=ISO-8859-1");
- DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(iso));
+ DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(iso), 0L, 0L);
StepVerifier.create(wrapper.getBodyAsString()).expectNext(text).verifyComplete();
}
@@ -140,7 +143,7 @@ public void getBodyAsStringDetectsUtf8BomAndStripsIt() {
System.arraycopy(bom, 0, withBom, 0, bom.length);
System.arraycopy(payload, 0, withBom, bom.length, payload.length);
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(withBom));
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(withBom), 0L, 0L);
StepVerifier.create(wrapper.getBodyAsString()).expectNext(text).verifyComplete();
}
@@ -150,7 +153,7 @@ public void getBodyAsStringDecodesUsingProvidedCharset() {
String text = "ümlaut";
byte[] latin1 = text.getBytes(StandardCharsets.ISO_8859_1);
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(latin1));
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(latin1), 0L, 0L);
StepVerifier.create(wrapper.getBodyAsString(StandardCharsets.ISO_8859_1)).expectNext(text).verifyComplete();
}
@@ -160,7 +163,7 @@ public void inheritedGetBodyAsInputStreamUsesDecodedBytes() throws IOException {
// Base getBodyAsInputStream() routes through getBodyAsByteArray(), so the override is exercised end-to-end.
byte[] decoded = bytes("decoded stream");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
try (InputStream stream = wrapper.getBodyAsInputStream().block()) {
assertNotNull(stream);
@@ -172,7 +175,7 @@ public void inheritedGetBodyAsInputStreamUsesDecodedBytes() throws IOException {
public void inheritedWriteBodyToWritesDecodedBytes() throws IOException {
byte[] decoded = bytes("write me");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
ByteArrayOutputStream sink = new ByteArrayOutputStream();
try (WritableByteChannel channel = Channels.newChannel(sink)) {
@@ -186,7 +189,7 @@ public void inheritedWriteBodyToWritesDecodedBytes() throws IOException {
public void inheritedBufferReturnsResponseBackedByDecodedBytes() {
byte[] decoded = bytes("buffered");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
HttpResponse buffered = wrapper.buffer();
assertNotNull(buffered);
@@ -201,15 +204,34 @@ public void inheritedGetBodyAsBinaryDataReturnsDecodedBytes() {
// must contain the decoded payload, not the original wire body. A divergent Content-Length header is set
// to make the wire vs decoded distinction explicit and guard against regressions in header forwarding.
byte[] decoded = bytes("decoded payload");
- HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(decoded.length + 32));
- DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, h, bytes("encoded wire body")), fluxOf(decoded));
+ long originalSize = decoded.length + 32;
+ HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(originalSize));
+ DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, bytes("encoded wire body")), fluxOf(decoded),
+ originalSize, decoded.length);
BinaryData data = wrapper.getBodyAsBinaryData();
assertNotNull(data);
assertArrayEquals(decoded, data.toBytes());
}
+ @Test
+ public void contentLengthIsOverriddenToDecodedSizeAndOriginalIsPreserved() {
+ long originalSize = 500L;
+ long decodedSize = 300L;
+ HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(originalSize))
+ .set(CUSTOM_HEADER, "preserve-me");
+ DecodedResponse wrapper
+ = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]), originalSize, decodedSize);
+
+ assertEquals(String.valueOf(decodedSize), wrapper.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+ assertEquals(String.valueOf(originalSize),
+ wrapper.getHeaders().getValue(Constants.HeaderConstants.ORIGINAL_CONTENT_LENGTH_HEADER_NAME));
+ assertEquals("preserve-me", wrapper.getHeaders().getValue(CUSTOM_HEADER));
+ // Deprecated getHeaderValue must reflect the same overrides.
+ assertEquals(String.valueOf(decodedSize),
+ wrapper.getHeaderValue(HttpHeaderName.CONTENT_LENGTH.getCaseInsensitiveName()));
+ }
+
private static byte[] readAll(InputStream stream) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
From c3449805d634879b8fd86eb0a8cc437f8a4f8880 Mon Sep 17 00:00:00 2001
From: Local Merge
Date: Wed, 20 May 2026 16:19:33 +0530
Subject: [PATCH 02/10] content length override changes
---
.../models/BlobsDownloadHeaders.java | 34 --------------
.../blob/models/BlobDownloadHeaders.java | 20 --------
.../common/implementation/Constants.java | 5 --
.../common/policy/DecodedResponse.java | 9 +---
...StorageContentValidationDecoderPolicy.java | 7 ++-
.../common/policy/DecodedResponseTests.java | 46 +++++++++----------
6 files changed, 29 insertions(+), 92 deletions(-)
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java
index b060c4c88c33..b45d6520f1ee 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java
@@ -301,11 +301,6 @@ public final class BlobsDownloadHeaders {
@Generated
private byte[] xMsContentCrc64;
- /*
- * The x-ms-original-content-length property.
- */
- private Long xMsOriginalContentLength;
-
private static final HttpHeaderName X_MS_CREATION_TIME = HttpHeaderName.fromString("x-ms-creation-time");
private static final HttpHeaderName X_MS_OR_POLICY_ID = HttpHeaderName.fromString("x-ms-or-policy-id");
@@ -374,9 +369,6 @@ public final class BlobsDownloadHeaders {
private static final HttpHeaderName X_MS_CONTENT_CRC64 = HttpHeaderName.fromString("x-ms-content-crc64");
- private static final HttpHeaderName X_MS_ORIGINAL_CONTENT_LENGTH
- = HttpHeaderName.fromString("x-ms-original-content-length");
-
// HttpHeaders containing the raw property values.
/**
* Creates an instance of BlobsDownloadHeaders class.
@@ -543,12 +535,6 @@ public BlobsDownloadHeaders(HttpHeaders rawHeaders) {
} else {
this.xMsContentCrc64 = null;
}
- String xMsOriginalContentLength = rawHeaders.getValue(X_MS_ORIGINAL_CONTENT_LENGTH);
- if (xMsOriginalContentLength != null) {
- this.xMsOriginalContentLength = Long.parseLong(xMsOriginalContentLength);
- } else {
- this.xMsOriginalContentLength = null;
- }
Map xMsMetaHeaderCollection = new LinkedHashMap<>();
Map xMsOrHeaderCollection = new LinkedHashMap<>();
@@ -1619,24 +1605,4 @@ public BlobsDownloadHeaders setXMsContentCrc64(byte[] xMsContentCrc64) {
this.xMsContentCrc64 = CoreUtils.clone(xMsContentCrc64);
return this;
}
-
- /**
- * Get the xMsOriginalContentLength property: The wire size of the encoded structured message body before decoding.
- *
- * @return the xMsOriginalContentLength value.
- */
- public Long getXMsOriginalContentLength() {
- return this.xMsOriginalContentLength;
- }
-
- /**
- * Set the xMsOriginalContentLength property: The wire size of the encoded structured message body before decoding.
- *
- * @param xMsOriginalContentLength the xMsOriginalContentLength value to set.
- * @return the BlobsDownloadHeaders object itself.
- */
- public BlobsDownloadHeaders setXMsOriginalContentLength(Long xMsOriginalContentLength) {
- this.xMsOriginalContentLength = xMsOriginalContentLength;
- return this;
- }
}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
index 812baceb423b..b47e47c3298c 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
@@ -1045,24 +1045,4 @@ public BlobDownloadHeaders setCreationTime(OffsetDateTime creationTime) {
return this;
}
- /**
- * Get the originalContentLength property: The wire size of the encoded structured message body before decoding.
- * Only present when content validation is active and the response was decoded transparently.
- *
- * @return the originalContentLength value.
- */
- public Long getOriginalContentLength() {
- return internalHeaders.getXMsOriginalContentLength();
- }
-
- /**
- * Set the originalContentLength property: The wire size of the encoded structured message body before decoding.
- *
- * @param originalContentLength the originalContentLength value to set.
- * @return the BlobDownloadHeaders object itself.
- */
- public BlobDownloadHeaders setOriginalContentLength(Long originalContentLength) {
- internalHeaders.setXMsOriginalContentLength(originalContentLength);
- return this;
- }
}
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java
index fe5bf72e11f7..c7b4f0198525 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java
@@ -264,11 +264,6 @@ public static final class HeaderConstants {
public static final HttpHeaderName STRUCTURED_CONTENT_LENGTH_HEADER_NAME
= HttpHeaderName.fromString(STRUCTURED_CONTENT_LENGTH);
- public static final String ORIGINAL_CONTENT_LENGTH = "x-ms-original-content-length";
-
- public static final HttpHeaderName ORIGINAL_CONTENT_LENGTH_HEADER_NAME
- = HttpHeaderName.fromString(ORIGINAL_CONTENT_LENGTH);
-
/**
* Metadata key ("hdi_isfolder") used to mark virtual directories in Azure Blob Storage.
*
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
index e852769598d2..99619c4feb88 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
@@ -8,7 +8,6 @@
import com.azure.core.http.HttpResponse;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.FluxUtil;
-import com.azure.storage.common.implementation.Constants;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -33,23 +32,19 @@ class DecodedResponse extends HttpResponse {
* Wraps {@code httpResponse} with a body backed by {@code decodedBody}.
*
* {@code Content-Length} is overridden to {@code decodedContentLength} so callers see the size of the bytes
- * they will actually read. The original wire size is preserved in {@code x-ms-original-content-length}.
+ * they will actually read from the decoded payload, not the larger wire size of the structured message.
*
* @param httpResponse The original response from the storage service.
* @param decodedBody The Flux of CRC-validated, framing-stripped payload bytes produced by the decoder pipeline.
- * @param originalContentLength The wire size of the encoded structured message body.
* @param decodedContentLength The size of the decoded payload that callers will consume.
*/
- DecodedResponse(HttpResponse httpResponse, Flux decodedBody, long originalContentLength,
- long decodedContentLength) {
+ DecodedResponse(HttpResponse httpResponse, Flux decodedBody, long decodedContentLength) {
super(httpResponse.getRequest());
this.originalResponse = httpResponse;
this.decodedBody = decodedBody;
HttpHeaders headers = new HttpHeaders();
httpResponse.getHeaders().stream().forEach(h -> headers.set(h.getName(), h.getValue()));
headers.set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(decodedContentLength));
- headers.set(Constants.HeaderConstants.ORIGINAL_CONTENT_LENGTH_HEADER_NAME,
- String.valueOf(originalContentLength));
this.adjustedHeaders = headers;
}
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
index a9017f6a7e05..bf77e449478b 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
@@ -74,13 +74,16 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN
validateStructuredMessageHeaders(httpResponse);
Long decodedContentLength = getStructuredContentLength(httpResponse.getHeaders());
+ if (decodedContentLength == null) {
+ throw LOGGER.logExceptionAsError(new IllegalStateException(
+ "x-ms-structured-content-length header is present but could not be parsed as a long."));
+ }
// Fresh decoder per response so retries each get a clean state machine.
StructuredMessageDecoder decoder = new StructuredMessageDecoder(contentLength);
Flux decodedStream = decodeStream(httpResponse.getBody(), decoder);
- // decodedContentLength is guaranteed non-null here: validateStructuredMessageHeaders confirmed its presence.
- return new DecodedResponse(httpResponse, decodedStream, contentLength, decodedContentLength);
+ return new DecodedResponse(httpResponse, decodedStream, decodedContentLength);
});
}
diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java
index 82939c292f15..c37e2bf414ba 100644
--- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java
+++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java
@@ -10,7 +10,6 @@
import com.azure.core.http.HttpResponse;
import com.azure.core.test.http.MockHttpResponse;
import com.azure.core.util.BinaryData;
-import com.azure.storage.common.implementation.Constants;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
@@ -65,7 +64,7 @@ public void preservesRequestStatusCodeAndHeaders() {
HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "100").set(CUSTOM_HEADER, "value");
MockHttpResponse original = mockResponse(206, h, bytes("encoded"));
- DecodedResponse wrapper = new DecodedResponse(original, fluxOf(bytes("decoded")), 100L, 80L);
+ DecodedResponse wrapper = new DecodedResponse(original, fluxOf(bytes("decoded")), 80L);
assertSame(original.getRequest(), wrapper.getRequest());
assertEquals(206, wrapper.getStatusCode());
@@ -77,7 +76,7 @@ public void preservesRequestStatusCodeAndHeaders() {
@Test
public void getHeaderValueByStringReturnsHeaderValue() {
HttpHeaders h = headers(CUSTOM_HEADER, "value");
- DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]), 0L, 0L);
+ DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]), 0L);
assertEquals("value", wrapper.getHeaderValue(CUSTOM_HEADER.getCaseInsensitiveName()));
assertNull(wrapper.getHeaderValue("nonexistent"));
@@ -87,7 +86,7 @@ public void getHeaderValueByStringReturnsHeaderValue() {
public void getBodyReturnsDecodedFlux() {
byte[] decoded = bytes("decoded body");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L);
StepVerifier.create(wrapper.getBody().reduce(new ByteArrayOutputStream(), (sink, buf) -> {
byte[] copy = new byte[buf.remaining()];
@@ -101,7 +100,7 @@ public void getBodyReturnsDecodedFlux() {
public void getBodyAsByteArrayReturnsDecodedBytes() {
byte[] decoded = bytes("decoded body");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L);
StepVerifier.create(wrapper.getBodyAsByteArray())
.expectNextMatches(b -> Arrays.equals(decoded, b))
@@ -114,7 +113,7 @@ public void getBodyAsStringDefaultsToUtf8WhenNoCharsetSpecified() {
// BOM nor a Content-Type charset parameter is present. This test pins the "no headers, no BOM" path.
String text = "héllo wörld – ✓";
DecodedResponse wrapper = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]),
- fluxOf(text.getBytes(StandardCharsets.UTF_8)), 0L, 0L);
+ fluxOf(text.getBytes(StandardCharsets.UTF_8)), 0L);
StepVerifier.create(wrapper.getBodyAsString()).expectNext(text).verifyComplete();
}
@@ -127,7 +126,7 @@ public void getBodyAsStringHonorsCharsetFromContentTypeHeader() {
String text = "ümlaut";
byte[] iso = text.getBytes(StandardCharsets.ISO_8859_1);
HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, "text/plain; charset=ISO-8859-1");
- DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(iso), 0L, 0L);
+ DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(iso), 0L);
StepVerifier.create(wrapper.getBodyAsString()).expectNext(text).verifyComplete();
}
@@ -143,7 +142,7 @@ public void getBodyAsStringDetectsUtf8BomAndStripsIt() {
System.arraycopy(bom, 0, withBom, 0, bom.length);
System.arraycopy(payload, 0, withBom, bom.length, payload.length);
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(withBom), 0L, 0L);
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(withBom), 0L);
StepVerifier.create(wrapper.getBodyAsString()).expectNext(text).verifyComplete();
}
@@ -153,7 +152,7 @@ public void getBodyAsStringDecodesUsingProvidedCharset() {
String text = "ümlaut";
byte[] latin1 = text.getBytes(StandardCharsets.ISO_8859_1);
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(latin1), 0L, 0L);
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(latin1), 0L);
StepVerifier.create(wrapper.getBodyAsString(StandardCharsets.ISO_8859_1)).expectNext(text).verifyComplete();
}
@@ -163,7 +162,7 @@ public void inheritedGetBodyAsInputStreamUsesDecodedBytes() throws IOException {
// Base getBodyAsInputStream() routes through getBodyAsByteArray(), so the override is exercised end-to-end.
byte[] decoded = bytes("decoded stream");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L);
try (InputStream stream = wrapper.getBodyAsInputStream().block()) {
assertNotNull(stream);
@@ -175,7 +174,7 @@ public void inheritedGetBodyAsInputStreamUsesDecodedBytes() throws IOException {
public void inheritedWriteBodyToWritesDecodedBytes() throws IOException {
byte[] decoded = bytes("write me");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L);
ByteArrayOutputStream sink = new ByteArrayOutputStream();
try (WritableByteChannel channel = Channels.newChannel(sink)) {
@@ -189,7 +188,7 @@ public void inheritedWriteBodyToWritesDecodedBytes() throws IOException {
public void inheritedBufferReturnsResponseBackedByDecodedBytes() {
byte[] decoded = bytes("buffered");
DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
+ = new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L);
HttpResponse buffered = wrapper.buffer();
assertNotNull(buffered);
@@ -204,10 +203,10 @@ public void inheritedGetBodyAsBinaryDataReturnsDecodedBytes() {
// must contain the decoded payload, not the original wire body. A divergent Content-Length header is set
// to make the wire vs decoded distinction explicit and guard against regressions in header forwarding.
byte[] decoded = bytes("decoded payload");
- long originalSize = decoded.length + 32;
- HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(originalSize));
- DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, bytes("encoded wire body")), fluxOf(decoded),
- originalSize, decoded.length);
+ long decodedSize = decoded.length;
+ HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(decodedSize + 32));
+ DecodedResponse wrapper
+ = new DecodedResponse(mockResponse(200, h, bytes("encoded wire body")), fluxOf(decoded), decodedSize);
BinaryData data = wrapper.getBodyAsBinaryData();
assertNotNull(data);
@@ -215,19 +214,18 @@ public void inheritedGetBodyAsBinaryDataReturnsDecodedBytes() {
}
@Test
- public void contentLengthIsOverriddenToDecodedSizeAndOriginalIsPreserved() {
- long originalSize = 500L;
+ public void contentLengthIsOverriddenToDecodedSize() {
+ long wireSize = 500L;
long decodedSize = 300L;
- HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(originalSize))
+ HttpHeaders h = new HttpHeaders()
+ .set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(wireSize))
.set(CUSTOM_HEADER, "preserve-me");
- DecodedResponse wrapper
- = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]), originalSize, decodedSize);
+ DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]),
+ decodedSize);
assertEquals(String.valueOf(decodedSize), wrapper.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
- assertEquals(String.valueOf(originalSize),
- wrapper.getHeaders().getValue(Constants.HeaderConstants.ORIGINAL_CONTENT_LENGTH_HEADER_NAME));
assertEquals("preserve-me", wrapper.getHeaders().getValue(CUSTOM_HEADER));
- // Deprecated getHeaderValue must reflect the same overrides.
+ // Deprecated getHeaderValue must reflect the same override.
assertEquals(String.valueOf(decodedSize),
wrapper.getHeaderValue(HttpHeaderName.CONTENT_LENGTH.getCaseInsensitiveName()));
}
From bf58dfea05f75fba16ee16f10006e38ef78dadee Mon Sep 17 00:00:00 2001
From: Local Merge
Date: Wed, 20 May 2026 16:43:04 +0530
Subject: [PATCH 03/10] removing stuff
---
.../blob/implementation/models/BlobsDownloadHeaders.java | 2 +-
.../java/com/azure/storage/blob/models/BlobDownloadHeaders.java | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java
index b45d6520f1ee..1d409a5a4cdd 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java
@@ -1596,7 +1596,7 @@ public byte[] getXMsContentCrc64() {
/**
* Set the xMsContentCrc64 property: The x-ms-content-crc64 property.
- *
+ *
* @param xMsContentCrc64 the xMsContentCrc64 value to set.
* @return the BlobsDownloadHeaders object itself.
*/
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
index b47e47c3298c..dfd93a535fc6 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
@@ -1044,5 +1044,4 @@ public BlobDownloadHeaders setCreationTime(OffsetDateTime creationTime) {
internalHeaders.setXMsCreationTime(creationTime);
return this;
}
-
}
From 6ecc2f826e6ee92aa6eb353dff1a5b34332d673d Mon Sep 17 00:00:00 2001
From: Local Merge
Date: Wed, 20 May 2026 20:34:55 +0530
Subject: [PATCH 04/10] addressing review comments
---
.../blob/models/BlobDownloadHeaders.java | 4 ++
.../common/policy/DecodedResponse.java | 10 ++---
...StorageContentValidationDecoderPolicy.java | 44 +++++++------------
.../common/policy/DecodedResponseTests.java | 7 ++-
4 files changed, 28 insertions(+), 37 deletions(-)
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
index dfd93a535fc6..a216c8a0f545 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
@@ -146,6 +146,10 @@ public List getObjectReplicationSourcePolicies() {
/**
* Get the contentLength property: The number of bytes present in the response body.
*
+ * When content validation is enabled, the storage client transparently decodes the structured message
+ * framing before returning the response. In that case this value reflects the size of the decoded payload
+ * that callers will actually read — not the larger wire size of the encoded message.
+ *
* @return the contentLength value.
*/
public Long getContentLength() {
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
index 99619c4feb88..d951ceeb4c94 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
@@ -15,13 +15,13 @@
import java.nio.charset.Charset;
/**
- * {@link HttpResponse} wrapper that exposes a decoded body stream while preserving the request, status code, and
- * headers of the original response.
+ * {@link HttpResponse} wrapper that exposes a decoded body stream while preserving the request and status code of
+ * the original response.
*
* The policy hands this class a Flux that already represents validated, framing-stripped bytes (produced by the
* decoder pipeline). This class's only job is to make that Flux look like the body of the original
- * {@link HttpResponse}. Status code, headers, and request remain identical to the underlying response so callers
- * cannot distinguish a validated download from a normal one – the validation is transparent.
+ * {@link HttpResponse}. {@code Content-Length} is overridden to the decoded payload size so it matches what callers
+ * will actually read; all other headers are forwarded verbatim. The validation is transparent to callers.
*/
class DecodedResponse extends HttpResponse {
private final HttpResponse originalResponse;
@@ -77,7 +77,7 @@ public Mono getBodyAsByteArray() {
@Override
public Mono getBodyAsString() {
return getBodyAsByteArray()
- .map(b -> CoreUtils.bomAwareToString(b, originalResponse.getHeaderValue(HttpHeaderName.CONTENT_TYPE)));
+ .map(b -> CoreUtils.bomAwareToString(b, adjustedHeaders.getValue(HttpHeaderName.CONTENT_TYPE)));
}
@Override
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
index bf77e449478b..f53c7981a269 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
@@ -70,14 +70,9 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN
return httpResponse;
}
- // Confirm the service actually honored our structured-body request before we hand the body to the decoder.
- validateStructuredMessageHeaders(httpResponse);
-
- Long decodedContentLength = getStructuredContentLength(httpResponse.getHeaders());
- if (decodedContentLength == null) {
- throw LOGGER.logExceptionAsError(new IllegalStateException(
- "x-ms-structured-content-length header is present but could not be parsed as a long."));
- }
+ // Confirm the service honored our structured-body request and parse the decoded length in one step,
+ // failing fast with a consistent error if either header is absent or unparseable.
+ long decodedContentLength = validateAndGetDecodedContentLength(httpResponse);
// Fresh decoder per response so retries each get a clean state machine.
StructuredMessageDecoder decoder = new StructuredMessageDecoder(contentLength);
@@ -98,11 +93,14 @@ private boolean shouldApplyDecoding(HttpPipelineCallContext context) {
}
/**
- * Verifies the response acknowledges the structured-body request: presence of the
- * {@code x-ms-structured-body} header and the {@code x-ms-structured-content-length}
- * header. If either is missing, the service is sending us a normal body and we must not run the decoder over it.
+ * Verifies the response acknowledges the structured-body request ({@code x-ms-structured-body} present) and
+ * parses {@code x-ms-structured-content-length} in one step. Throws with a consistent error message if either
+ * header is absent or the length value is not parseable as a {@code long}, matching the fail-fast behaviour
+ * used for other validation failures.
+ *
+ * @return the decoded payload size declared by the service.
*/
- private void validateStructuredMessageHeaders(HttpResponse httpResponse) {
+ private long validateAndGetDecodedContentLength(HttpResponse httpResponse) {
String structuredBody
= httpResponse.getHeaders().getValue(Constants.HeaderConstants.STRUCTURED_BODY_TYPE_HEADER_NAME);
String structuredContentLength
@@ -111,6 +109,12 @@ private void validateStructuredMessageHeaders(HttpResponse httpResponse) {
throw LOGGER.logExceptionAsError(
new IllegalStateException("Structured message was requested but the response did not acknowledge it."));
}
+ try {
+ return Long.parseLong(structuredContentLength);
+ } catch (NumberFormatException e) {
+ throw LOGGER.logExceptionAsError(new IllegalStateException("x-ms-structured-content-length header value '"
+ + structuredContentLength + "' could not be parsed as a long.", e));
+ }
}
/**
@@ -129,22 +133,6 @@ private static Long getContentLength(HttpHeaders headers) {
return null;
}
- /**
- * Reads {@code x-ms-structured-content-length} as a {@code Long}, returning {@code null} if missing or
- * unparseable.
- */
- private static Long getStructuredContentLength(HttpHeaders headers) {
- String value = headers.getValue(Constants.HeaderConstants.STRUCTURED_CONTENT_LENGTH_HEADER_NAME);
- if (value != null) {
- try {
- return Long.parseLong(value);
- } catch (NumberFormatException e) {
- // Malformed header; fall through to null.
- }
- }
- return null;
- }
-
/**
* @return true for a 2xx response to a GET request, the only response shape that carries a body we
* can decode. 206 (Partial Content) on retried range downloads is included.
diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java
index c37e2bf414ba..e99c169a1763 100644
--- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java
+++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java
@@ -217,11 +217,10 @@ public void inheritedGetBodyAsBinaryDataReturnsDecodedBytes() {
public void contentLengthIsOverriddenToDecodedSize() {
long wireSize = 500L;
long decodedSize = 300L;
- HttpHeaders h = new HttpHeaders()
- .set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(wireSize))
+ HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(wireSize))
.set(CUSTOM_HEADER, "preserve-me");
- DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]),
- decodedSize);
+ DecodedResponse wrapper
+ = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]), decodedSize);
assertEquals(String.valueOf(decodedSize), wrapper.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
assertEquals("preserve-me", wrapper.getHeaders().getValue(CUSTOM_HEADER));
From 4aab056732c5bea36f2d6199bfc5a5fef07a72fc Mon Sep 17 00:00:00 2001
From: Local Merge
Date: Wed, 20 May 2026 20:37:58 +0530
Subject: [PATCH 05/10] addressing review comments
---
.../com/azure/storage/blob/models/BlobDownloadHeaders.java | 4 ----
1 file changed, 4 deletions(-)
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
index a216c8a0f545..dfd93a535fc6 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
@@ -146,10 +146,6 @@ public List getObjectReplicationSourcePolicies() {
/**
* Get the contentLength property: The number of bytes present in the response body.
*
- * When content validation is enabled, the storage client transparently decodes the structured message
- * framing before returning the response. In that case this value reflects the size of the decoded payload
- * that callers will actually read — not the larger wire size of the encoded message.
- *
* @return the contentLength value.
*/
public Long getContentLength() {
From 7632ef5e5fbb75a2ce64a40a89e64aef085883c2 Mon Sep 17 00:00:00 2001
From: Local Merge
Date: Thu, 21 May 2026 15:48:11 +0530
Subject: [PATCH 06/10] addressing review comments
---
.../common/policy/DecodedResponse.java | 3 +-
...geContentValidationDecoderPolicyTests.java | 131 ++++++++++++++++++
2 files changed, 132 insertions(+), 2 deletions(-)
create mode 100644 sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
index d951ceeb4c94..7e88de8cd37e 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java
@@ -42,8 +42,7 @@ class DecodedResponse extends HttpResponse {
super(httpResponse.getRequest());
this.originalResponse = httpResponse;
this.decodedBody = decodedBody;
- HttpHeaders headers = new HttpHeaders();
- httpResponse.getHeaders().stream().forEach(h -> headers.set(h.getName(), h.getValue()));
+ HttpHeaders headers = new HttpHeaders(httpResponse.getHeaders());
headers.set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(decodedContentLength));
this.adjustedHeaders = headers;
}
diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
new file mode 100644
index 000000000000..e2d244b616b9
--- /dev/null
+++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
@@ -0,0 +1,131 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.common.policy;
+
+import com.azure.core.http.HttpHeaderName;
+import com.azure.core.http.HttpHeaders;
+import com.azure.core.http.HttpMethod;
+import com.azure.core.http.HttpPipeline;
+import com.azure.core.http.HttpPipelineBuilder;
+import com.azure.core.http.HttpRequest;
+import com.azure.core.http.HttpResponse;
+import com.azure.core.test.http.MockHttpResponse;
+import com.azure.core.util.Context;
+import com.azure.core.util.FluxUtil;
+import com.azure.storage.common.implementation.Constants;
+import com.azure.storage.common.implementation.contentvalidation.StructuredMessageConstants;
+import com.azure.storage.common.implementation.contentvalidation.StructuredMessageEncoder;
+import com.azure.storage.common.implementation.contentvalidation.StructuredMessageFlags;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.concurrent.ThreadLocalRandom;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class StorageContentValidationDecoderPolicyTests {
+
+ private static HttpRequest getRequest() {
+ try {
+ return new HttpRequest(HttpMethod.GET, new URL("http://example.com/blob"));
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static byte[] encodeToBytes(byte[] data, int segmentSize, StructuredMessageFlags flags) throws IOException {
+ StructuredMessageEncoder encoder = new StructuredMessageEncoder(data.length, segmentSize, flags);
+ Flux flux = encoder.encode(ByteBuffer.wrap(data));
+ ByteBuffer encoded
+ = ByteBuffer.wrap(Objects.requireNonNull(FluxUtil.collectBytesInByteBufferStream(flux).block()));
+ byte[] bytes = new byte[encoded.remaining()];
+ encoded.get(bytes);
+ return bytes;
+ }
+
+ @Test
+ public void contentLengthIsOverriddenToDecodedSizeWhenDecodingApplied() throws IOException {
+ byte[] payload = new byte[64];
+ ThreadLocalRandom.current().nextBytes(payload);
+
+ byte[] encoded = encodeToBytes(payload, 64, StructuredMessageFlags.STORAGE_CRC64);
+ long encodedLen = encoded.length;
+ long decodedLen = payload.length;
+
+ HttpHeaders responseHeaders = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(encodedLen))
+ .set(Constants.HeaderConstants.STRUCTURED_BODY_TYPE_HEADER_NAME,
+ StructuredMessageConstants.STRUCTURED_BODY_TYPE_VALUE)
+ .set(Constants.HeaderConstants.STRUCTURED_CONTENT_LENGTH_HEADER_NAME, String.valueOf(decodedLen));
+
+ HttpPipeline pipeline = new HttpPipelineBuilder().policies(new StorageContentValidationDecoderPolicy())
+ .httpClient(request -> Mono.just(new MockHttpResponse(request, 200, responseHeaders, encoded)))
+ .build();
+
+ Context ctx = new Context(StructuredMessageConstants.STRUCTURED_MESSAGE_DECODING_CONTEXT_KEY, true);
+ HttpResponse response = pipeline.send(getRequest(), ctx).block();
+
+ assertNotNull(response);
+ assertEquals(String.valueOf(decodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+ }
+
+ @Test
+ public void contentLengthMatchesActualDecodedBodySize() throws IOException {
+ byte[] payload = new byte[128];
+ ThreadLocalRandom.current().nextBytes(payload);
+
+ byte[] encoded = encodeToBytes(payload, 64, StructuredMessageFlags.STORAGE_CRC64);
+ long encodedLen = encoded.length;
+ long decodedLen = payload.length;
+
+ HttpHeaders responseHeaders = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(encodedLen))
+ .set(Constants.HeaderConstants.STRUCTURED_BODY_TYPE_HEADER_NAME,
+ StructuredMessageConstants.STRUCTURED_BODY_TYPE_VALUE)
+ .set(Constants.HeaderConstants.STRUCTURED_CONTENT_LENGTH_HEADER_NAME, String.valueOf(decodedLen));
+
+ HttpPipeline pipeline = new HttpPipelineBuilder().policies(new StorageContentValidationDecoderPolicy())
+ .httpClient(request -> Mono.just(new MockHttpResponse(request, 200, responseHeaders, encoded)))
+ .build();
+
+ Context ctx = new Context(StructuredMessageConstants.STRUCTURED_MESSAGE_DECODING_CONTEXT_KEY, true);
+ HttpResponse response = pipeline.send(getRequest(), ctx).block();
+
+ assertNotNull(response);
+ assertEquals(String.valueOf(decodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+
+ // Consume the body and verify it matches the original payload — the reported Content-Length
+ // must equal the actual number of bytes produced by the decoder.
+ byte[] body = Objects.requireNonNull(FluxUtil.collectBytesInByteBufferStream(response.getBody()).block());
+ assertEquals(decodedLen, body.length);
+ assertArrayEquals(payload, body);
+ }
+
+ @Test
+ public void contentLengthIsUnchangedWhenDecodingNotApplied() throws IOException {
+ byte[] payload = new byte[64];
+ ThreadLocalRandom.current().nextBytes(payload);
+
+ byte[] encoded = encodeToBytes(payload, 64, StructuredMessageFlags.STORAGE_CRC64);
+ long encodedLen = encoded.length;
+
+ HttpHeaders responseHeaders = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(encodedLen));
+
+ HttpPipeline pipeline = new HttpPipelineBuilder().policies(new StorageContentValidationDecoderPolicy())
+ .httpClient(request -> Mono.just(new MockHttpResponse(request, 200, responseHeaders, encoded)))
+ .build();
+
+ // No decoding context flag set — policy should pass the response through unchanged.
+ HttpResponse response = pipeline.send(getRequest()).block();
+
+ assertNotNull(response);
+ assertEquals(String.valueOf(encodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+ }
+}
From 03fe291ccea9954e5898efbc912f5e807466482d Mon Sep 17 00:00:00 2001
From: Local Merge
Date: Thu, 21 May 2026 18:30:04 +0530
Subject: [PATCH 07/10] resolving merge conflicts
---
...geContentValidationDecoderPolicyTests.java | 362 +++++++++---------
1 file changed, 181 insertions(+), 181 deletions(-)
diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
index 537071f38917..1480bb647039 100644
--- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
+++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
@@ -1,184 +1,184 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-
- package com.azure.storage.common.policy;
-
- import com.azure.core.http.HttpClient;
- import com.azure.core.http.HttpHeaderName;
- import com.azure.core.http.HttpHeaders;
- import com.azure.core.http.HttpMethod;
- import com.azure.core.http.HttpPipeline;
- import com.azure.core.http.HttpPipelineBuilder;
- import com.azure.core.http.HttpRequest;
- import com.azure.core.http.HttpResponse;
- import com.azure.core.test.http.MockHttpResponse;
- import com.azure.core.util.Context;
- import com.azure.core.util.FluxUtil;
- import com.azure.storage.common.implementation.Constants;
- import com.azure.storage.common.implementation.contentvalidation.StructuredMessageConstants;
- import com.azure.storage.common.implementation.contentvalidation.StructuredMessageEncoder;
- import com.azure.storage.common.implementation.contentvalidation.StructuredMessageFlags;
- import org.junit.jupiter.api.Test;
- import org.junit.jupiter.params.ParameterizedTest;
- import org.junit.jupiter.params.provider.Arguments;
- import org.junit.jupiter.params.provider.MethodSource;
- import reactor.core.publisher.Flux;
- import reactor.core.publisher.Mono;
-
- import java.io.IOException;
- import java.nio.ByteBuffer;
- import java.util.Objects;
- import java.util.concurrent.ThreadLocalRandom;
- import java.util.concurrent.atomic.AtomicReference;
- import java.util.stream.Stream;
-
- import static org.junit.jupiter.api.Assertions.assertArrayEquals;
- import static org.junit.jupiter.api.Assertions.assertEquals;
- import static org.junit.jupiter.api.Assertions.assertNotNull;
- import static org.junit.jupiter.api.Assertions.assertTrue;
-
- /**
- * Tests {@link StorageContentValidationDecoderPolicy} together with {@link StructuredMessageEncoder} /
- * wire-format payloads so the reactive decode path matches what the blob download pipeline uses.
- */
- public class StorageContentValidationDecoderPolicyTests {
-
- /**
- * End-to-end through the policy: encoded body uses multi-megabyte segment payload lengths (not the default
- * 4 MiB framing only); decoded flux must match the original bytes.
- */
- @ParameterizedTest
- @MethodSource("segmentPayloadSizeAndTotalPayloadSizeSupplier")
- public void decodesDynamicallySizedSegmentStructuredMessageThroughPipeline(int segmentPayloadSize,
- int totalPayloadSize) throws IOException {
- byte[] originalData = new byte[totalPayloadSize];
- ThreadLocalRandom.current().nextBytes(originalData);
-
- byte[] encodedBytes
- = encodeStructuredMessageWireBytes(originalData, segmentPayloadSize, StructuredMessageFlags.STORAGE_CRC64);
-
- AtomicReference requestAfterPolicies = new AtomicReference<>();
- HttpClient httpClient = request -> {
- requestAfterPolicies.set(request);
- HttpHeaders headers = structuredDownloadResponseHeaders(encodedBytes.length, totalPayloadSize);
- return Mono.just(new MockHttpResponse(request, 200, headers, encodedBytes));
- };
-
- HttpPipeline pipeline = new HttpPipelineBuilder().policies((context, next) -> {
- context.setData(StructuredMessageConstants.STRUCTURED_MESSAGE_DECODING_CONTEXT_KEY, true);
- return next.process();
- }, new StorageContentValidationDecoderPolicy()).httpClient(httpClient).build();
-
- HttpRequest request = new HttpRequest(HttpMethod.GET, "https://example.blob.core.windows.net/c/b");
- try (HttpResponse response = pipeline.send(request, Context.NONE).block()) {
- assertNotNull(response);
- assertTrue(response instanceof DecodedResponse);
- byte[] decoded = Objects.requireNonNull(response.getBodyAsByteArray().block());
- assertArrayEquals(originalData, decoded);
- }
-
- HttpRequest sent = requestAfterPolicies.get();
- assertNotNull(sent);
- assertEquals(StructuredMessageConstants.STRUCTURED_BODY_TYPE_VALUE,
- sent.getHeaders().getValue(Constants.HeaderConstants.STRUCTURED_BODY_TYPE_HEADER_NAME));
- }
-
- @Test
- public void contentLengthIsOverriddenToDecodedSizeWhenDecodingApplied() throws IOException {
- byte[] payload = new byte[64];
- ThreadLocalRandom.current().nextBytes(payload);
-
- byte[] encoded = encodeStructuredMessageWireBytes(payload, 64, StructuredMessageFlags.STORAGE_CRC64);
- long encodedLen = encoded.length;
- long decodedLen = payload.length;
-
- HttpClient httpClient = request -> Mono.just(
- new MockHttpResponse(request, 200, structuredDownloadResponseHeaders(encodedLen, decodedLen), encoded));
-
- HttpPipeline pipeline = new HttpPipelineBuilder().policies((context, next) -> {
- context.setData(StructuredMessageConstants.STRUCTURED_MESSAGE_DECODING_CONTEXT_KEY, true);
- return next.process();
- }, new StorageContentValidationDecoderPolicy()).httpClient(httpClient).build();
-
- HttpRequest request = new HttpRequest(HttpMethod.GET, "https://example.blob.core.windows.net/c/b");
- HttpResponse response = pipeline.send(request, Context.NONE).block();
-
- assertNotNull(response);
- assertEquals(String.valueOf(decodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
- }
-
- @Test
- public void contentLengthMatchesActualDecodedBodySize() throws IOException {
- byte[] payload = new byte[128];
- ThreadLocalRandom.current().nextBytes(payload);
-
- byte[] encoded = encodeStructuredMessageWireBytes(payload, 64, StructuredMessageFlags.STORAGE_CRC64);
- long encodedLen = encoded.length;
- long decodedLen = payload.length;
-
- HttpClient httpClient = request -> Mono.just(
- new MockHttpResponse(request, 200, structuredDownloadResponseHeaders(encodedLen, decodedLen), encoded));
-
- HttpPipeline pipeline = new HttpPipelineBuilder().policies((context, next) -> {
- context.setData(StructuredMessageConstants.STRUCTURED_MESSAGE_DECODING_CONTEXT_KEY, true);
- return next.process();
- }, new StorageContentValidationDecoderPolicy()).httpClient(httpClient).build();
-
- HttpRequest request = new HttpRequest(HttpMethod.GET, "https://example.blob.core.windows.net/c/b");
- try (HttpResponse response = pipeline.send(request, Context.NONE).block()) {
- assertNotNull(response);
- assertEquals(String.valueOf(decodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
-
- byte[] body = Objects.requireNonNull(FluxUtil.collectBytesInByteBufferStream(response.getBody()).block());
- assertEquals(decodedLen, body.length);
- assertArrayEquals(payload, body);
- }
- }
-
- @Test
- public void contentLengthIsUnchangedWhenDecodingNotApplied() throws IOException {
- byte[] payload = new byte[64];
- ThreadLocalRandom.current().nextBytes(payload);
-
- byte[] encoded = encodeStructuredMessageWireBytes(payload, 64, StructuredMessageFlags.STORAGE_CRC64);
- long encodedLen = encoded.length;
-
- HttpHeaders responseHeaders = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(encodedLen));
- HttpClient httpClient = request -> Mono.just(new MockHttpResponse(request, 200, responseHeaders, encoded));
-
- HttpPipeline pipeline = new HttpPipelineBuilder()
- .policies(new StorageContentValidationDecoderPolicy())
- .httpClient(httpClient)
- .build();
-
- HttpRequest request = new HttpRequest(HttpMethod.GET, "https://example.blob.core.windows.net/c/b");
- HttpResponse response = pipeline.send(request, Context.NONE).block();
-
- assertNotNull(response);
- assertEquals(String.valueOf(encodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
- }
-
- private static Stream segmentPayloadSizeAndTotalPayloadSizeSupplier() {
- return Stream.of(Arguments.of(10 * 1024 * 1024, 10 * 1024 * 1024 + 1), // larger than 4 MiB
- Arguments.of(3 * 1024 * 1024, 3 * 1024 * 1024 + 1), // smaller than 4 MiB, but not KB
- Arguments.of(5 * 1024 * 1024 + 1, 15 * 1024 * 1024));
- }
-
- private static HttpHeaders structuredDownloadResponseHeaders(long contentLength, long structuredContentLength) {
- HttpHeaders headers = new HttpHeaders();
- headers.set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(contentLength));
- headers.set(Constants.HeaderConstants.STRUCTURED_BODY_TYPE_HEADER_NAME,
- StructuredMessageConstants.STRUCTURED_BODY_TYPE_VALUE);
- headers.set(Constants.HeaderConstants.STRUCTURED_CONTENT_LENGTH_HEADER_NAME,
- String.valueOf(structuredContentLength));
- return headers;
- }
-
- private static byte[] encodeStructuredMessageWireBytes(byte[] originalData, int segmentLength,
- StructuredMessageFlags flags) throws IOException {
- StructuredMessageEncoder encoder = new StructuredMessageEncoder(originalData.length, segmentLength, flags);
- Flux flux = encoder.encode(ByteBuffer.wrap(originalData));
- return Objects.requireNonNull(FluxUtil.collectBytesInByteBufferStream(flux).block());
- }
- }
+package com.azure.storage.common.policy;
+
+import com.azure.core.http.HttpClient;
+import com.azure.core.http.HttpHeaderName;
+import com.azure.core.http.HttpHeaders;
+import com.azure.core.http.HttpMethod;
+import com.azure.core.http.HttpPipeline;
+import com.azure.core.http.HttpPipelineBuilder;
+import com.azure.core.http.HttpRequest;
+import com.azure.core.http.HttpResponse;
+import com.azure.core.test.http.MockHttpResponse;
+import com.azure.core.util.Context;
+import com.azure.core.util.FluxUtil;
+import com.azure.storage.common.implementation.Constants;
+import com.azure.storage.common.implementation.contentvalidation.StructuredMessageConstants;
+import com.azure.storage.common.implementation.contentvalidation.StructuredMessageEncoder;
+import com.azure.storage.common.implementation.contentvalidation.StructuredMessageFlags;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests {@link StorageContentValidationDecoderPolicy} together with {@link StructuredMessageEncoder} /
+ * wire-format payloads so the reactive decode path matches what the blob download pipeline uses.
+ */
+public class StorageContentValidationDecoderPolicyTests {
+
+ /**
+ * End-to-end through the policy: encoded body uses multi-megabyte segment payload lengths (not the default
+ * 4 MiB framing only); decoded flux must match the original bytes.
+ */
+ @ParameterizedTest
+ @MethodSource("segmentPayloadSizeAndTotalPayloadSizeSupplier")
+ public void decodesDynamicallySizedSegmentStructuredMessageThroughPipeline(int segmentPayloadSize,
+ int totalPayloadSize) throws IOException {
+ byte[] originalData = new byte[totalPayloadSize];
+ ThreadLocalRandom.current().nextBytes(originalData);
+
+ byte[] encodedBytes
+ = encodeStructuredMessageWireBytes(originalData, segmentPayloadSize, StructuredMessageFlags.STORAGE_CRC64);
+
+ AtomicReference requestAfterPolicies = new AtomicReference<>();
+ HttpClient httpClient = request -> {
+ requestAfterPolicies.set(request);
+ HttpHeaders headers = structuredDownloadResponseHeaders(encodedBytes.length, totalPayloadSize);
+ return Mono.just(new MockHttpResponse(request, 200, headers, encodedBytes));
+ };
+
+ HttpPipeline pipeline = new HttpPipelineBuilder().policies((context, next) -> {
+ context.setData(StructuredMessageConstants.STRUCTURED_MESSAGE_DECODING_CONTEXT_KEY, true);
+ return next.process();
+ }, new StorageContentValidationDecoderPolicy()).httpClient(httpClient).build();
+
+ HttpRequest request = new HttpRequest(HttpMethod.GET, "https://example.blob.core.windows.net/c/b");
+ try (HttpResponse response = pipeline.send(request, Context.NONE).block()) {
+ assertNotNull(response);
+ assertTrue(response instanceof DecodedResponse);
+ byte[] decoded = Objects.requireNonNull(response.getBodyAsByteArray().block());
+ assertArrayEquals(originalData, decoded);
+ }
+
+ HttpRequest sent = requestAfterPolicies.get();
+ assertNotNull(sent);
+ assertEquals(StructuredMessageConstants.STRUCTURED_BODY_TYPE_VALUE,
+ sent.getHeaders().getValue(Constants.HeaderConstants.STRUCTURED_BODY_TYPE_HEADER_NAME));
+ }
+
+ @Test
+ public void contentLengthIsOverriddenToDecodedSizeWhenDecodingApplied() throws IOException {
+ byte[] payload = new byte[64];
+ ThreadLocalRandom.current().nextBytes(payload);
+
+ byte[] encoded = encodeStructuredMessageWireBytes(payload, 64, StructuredMessageFlags.STORAGE_CRC64);
+ long encodedLen = encoded.length;
+ long decodedLen = payload.length;
+
+ HttpClient httpClient = request -> Mono.just(
+ new MockHttpResponse(request, 200, structuredDownloadResponseHeaders(encodedLen, decodedLen), encoded));
+
+ HttpPipeline pipeline = new HttpPipelineBuilder().policies((context, next) -> {
+ context.setData(StructuredMessageConstants.STRUCTURED_MESSAGE_DECODING_CONTEXT_KEY, true);
+ return next.process();
+ }, new StorageContentValidationDecoderPolicy()).httpClient(httpClient).build();
+
+ HttpRequest request = new HttpRequest(HttpMethod.GET, "https://example.blob.core.windows.net/c/b");
+ HttpResponse response = pipeline.send(request, Context.NONE).block();
+
+ assertNotNull(response);
+ assertEquals(String.valueOf(decodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+ }
+
+ @Test
+ public void contentLengthMatchesActualDecodedBodySize() throws IOException {
+ byte[] payload = new byte[128];
+ ThreadLocalRandom.current().nextBytes(payload);
+
+ byte[] encoded = encodeStructuredMessageWireBytes(payload, 64, StructuredMessageFlags.STORAGE_CRC64);
+ long encodedLen = encoded.length;
+ long decodedLen = payload.length;
+
+ HttpClient httpClient = request -> Mono.just(
+ new MockHttpResponse(request, 200, structuredDownloadResponseHeaders(encodedLen, decodedLen), encoded));
+
+ HttpPipeline pipeline = new HttpPipelineBuilder().policies((context, next) -> {
+ context.setData(StructuredMessageConstants.STRUCTURED_MESSAGE_DECODING_CONTEXT_KEY, true);
+ return next.process();
+ }, new StorageContentValidationDecoderPolicy()).httpClient(httpClient).build();
+
+ HttpRequest request = new HttpRequest(HttpMethod.GET, "https://example.blob.core.windows.net/c/b");
+ try (HttpResponse response = pipeline.send(request, Context.NONE).block()) {
+ assertNotNull(response);
+ assertEquals(String.valueOf(decodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+
+ byte[] body = Objects.requireNonNull(FluxUtil.collectBytesInByteBufferStream(response.getBody()).block());
+ assertEquals(decodedLen, body.length);
+ assertArrayEquals(payload, body);
+ }
+ }
+
+ @Test
+ public void contentLengthIsUnchangedWhenDecodingNotApplied() throws IOException {
+ byte[] payload = new byte[64];
+ ThreadLocalRandom.current().nextBytes(payload);
+
+ byte[] encoded = encodeStructuredMessageWireBytes(payload, 64, StructuredMessageFlags.STORAGE_CRC64);
+ long encodedLen = encoded.length;
+
+ HttpHeaders responseHeaders = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(encodedLen));
+ HttpClient httpClient = request -> Mono.just(new MockHttpResponse(request, 200, responseHeaders, encoded));
+
+ HttpPipeline pipeline = new HttpPipelineBuilder()
+ .policies(new StorageContentValidationDecoderPolicy())
+ .httpClient(httpClient)
+ .build();
+
+ HttpRequest request = new HttpRequest(HttpMethod.GET, "https://example.blob.core.windows.net/c/b");
+ HttpResponse response = pipeline.send(request, Context.NONE).block();
+
+ assertNotNull(response);
+ assertEquals(String.valueOf(encodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+ }
+
+ private static Stream segmentPayloadSizeAndTotalPayloadSizeSupplier() {
+ return Stream.of(Arguments.of(10 * 1024 * 1024, 10 * 1024 * 1024 + 1), // larger than 4 MiB
+ Arguments.of(3 * 1024 * 1024, 3 * 1024 * 1024 + 1), // smaller than 4 MiB, but not KB
+ Arguments.of(5 * 1024 * 1024 + 1, 15 * 1024 * 1024));
+ }
+
+ private static HttpHeaders structuredDownloadResponseHeaders(long contentLength, long structuredContentLength) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(contentLength));
+ headers.set(Constants.HeaderConstants.STRUCTURED_BODY_TYPE_HEADER_NAME,
+ StructuredMessageConstants.STRUCTURED_BODY_TYPE_VALUE);
+ headers.set(Constants.HeaderConstants.STRUCTURED_CONTENT_LENGTH_HEADER_NAME,
+ String.valueOf(structuredContentLength));
+ return headers;
+ }
+
+ private static byte[] encodeStructuredMessageWireBytes(byte[] originalData, int segmentLength,
+ StructuredMessageFlags flags) throws IOException {
+ StructuredMessageEncoder encoder = new StructuredMessageEncoder(originalData.length, segmentLength, flags);
+ Flux flux = encoder.encode(ByteBuffer.wrap(originalData));
+ return Objects.requireNonNull(FluxUtil.collectBytesInByteBufferStream(flux).block());
+ }
+}
From e0c23360039ff64c9bc224157a95a58a6db39bd8 Mon Sep 17 00:00:00 2001
From: Isabelle <141270045+ibrandes@users.noreply.github.com>
Date: Thu, 21 May 2026 09:52:10 -0700
Subject: [PATCH 08/10] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
.../StorageContentValidationDecoderPolicyTests.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
index 1480bb647039..e88007571b5f 100644
--- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
+++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
@@ -102,10 +102,10 @@ public void contentLengthIsOverriddenToDecodedSizeWhenDecodingApplied() throws I
}, new StorageContentValidationDecoderPolicy()).httpClient(httpClient).build();
HttpRequest request = new HttpRequest(HttpMethod.GET, "https://example.blob.core.windows.net/c/b");
- HttpResponse response = pipeline.send(request, Context.NONE).block();
-
- assertNotNull(response);
- assertEquals(String.valueOf(decodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+ try (HttpResponse response = pipeline.send(request, Context.NONE).block()) {
+ assertNotNull(response);
+ assertEquals(String.valueOf(decodedLen), response.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
+ }
}
@Test
From f767eaa3d00f8cea3d9b50030d09a989f204339f Mon Sep 17 00:00:00 2001
From: Local Merge
Date: Fri, 22 May 2026 11:44:11 +0530
Subject: [PATCH 09/10] fixing spotbugs
---
.../policy/StorageContentValidationDecoderPolicyTests.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
index e88007571b5f..6b0b8474ee5c 100644
--- a/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
+++ b/sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicyTests.java
@@ -147,8 +147,7 @@ public void contentLengthIsUnchangedWhenDecodingNotApplied() throws IOException
HttpHeaders responseHeaders = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(encodedLen));
HttpClient httpClient = request -> Mono.just(new MockHttpResponse(request, 200, responseHeaders, encoded));
- HttpPipeline pipeline = new HttpPipelineBuilder()
- .policies(new StorageContentValidationDecoderPolicy())
+ HttpPipeline pipeline = new HttpPipelineBuilder().policies(new StorageContentValidationDecoderPolicy())
.httpClient(httpClient)
.build();
From 0d37bd1520c87ef3cdd540dd99bd960581a61e06 Mon Sep 17 00:00:00 2001
From: Local Merge
Date: Fri, 22 May 2026 12:13:13 +0530
Subject: [PATCH 10/10] fixing spellchecks
---
.../common/policy/StorageContentValidationDecoderPolicy.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
index 45a08284871c..21150174a03a 100644
--- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
+++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java
@@ -71,7 +71,7 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN
}
// Confirm the service honored our structured-body request and parse the decoded length in one step,
- // failing fast with a consistent error if either header is absent or unparseable.
+ // failing fast with a consistent error if either header is absent or not parseable as a long.
long decodedContentLength = validateAndGetDecodedContentLength(httpResponse);
// Fresh decoder per response so retries each get a clean state machine.