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.