Skip to content

Commit 81d9e61

Browse files
author
Local Merge
committed
content length override changes
1 parent 3a287fc commit 81d9e61

6 files changed

Lines changed: 137 additions & 24 deletions

File tree

sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,11 @@ public final class BlobsDownloadHeaders {
301301
@Generated
302302
private byte[] xMsContentCrc64;
303303

304+
/*
305+
* The x-ms-original-content-length property.
306+
*/
307+
private Long xMsOriginalContentLength;
308+
304309
private static final HttpHeaderName X_MS_CREATION_TIME = HttpHeaderName.fromString("x-ms-creation-time");
305310

306311
private static final HttpHeaderName X_MS_OR_POLICY_ID = HttpHeaderName.fromString("x-ms-or-policy-id");
@@ -369,6 +374,9 @@ public final class BlobsDownloadHeaders {
369374

370375
private static final HttpHeaderName X_MS_CONTENT_CRC64 = HttpHeaderName.fromString("x-ms-content-crc64");
371376

377+
private static final HttpHeaderName X_MS_ORIGINAL_CONTENT_LENGTH
378+
= HttpHeaderName.fromString("x-ms-original-content-length");
379+
372380
// HttpHeaders containing the raw property values.
373381
/**
374382
* Creates an instance of BlobsDownloadHeaders class.
@@ -535,6 +543,12 @@ public BlobsDownloadHeaders(HttpHeaders rawHeaders) {
535543
} else {
536544
this.xMsContentCrc64 = null;
537545
}
546+
String xMsOriginalContentLength = rawHeaders.getValue(X_MS_ORIGINAL_CONTENT_LENGTH);
547+
if (xMsOriginalContentLength != null) {
548+
this.xMsOriginalContentLength = Long.parseLong(xMsOriginalContentLength);
549+
} else {
550+
this.xMsOriginalContentLength = null;
551+
}
538552
Map<String, String> xMsMetaHeaderCollection = new LinkedHashMap<>();
539553
Map<String, String> xMsOrHeaderCollection = new LinkedHashMap<>();
540554

@@ -1596,7 +1610,7 @@ public byte[] getXMsContentCrc64() {
15961610

15971611
/**
15981612
* Set the xMsContentCrc64 property: The x-ms-content-crc64 property.
1599-
*
1613+
*
16001614
* @param xMsContentCrc64 the xMsContentCrc64 value to set.
16011615
* @return the BlobsDownloadHeaders object itself.
16021616
*/
@@ -1605,4 +1619,24 @@ public BlobsDownloadHeaders setXMsContentCrc64(byte[] xMsContentCrc64) {
16051619
this.xMsContentCrc64 = CoreUtils.clone(xMsContentCrc64);
16061620
return this;
16071621
}
1622+
1623+
/**
1624+
* Get the xMsOriginalContentLength property: The wire size of the encoded structured message body before decoding.
1625+
*
1626+
* @return the xMsOriginalContentLength value.
1627+
*/
1628+
public Long getXMsOriginalContentLength() {
1629+
return this.xMsOriginalContentLength;
1630+
}
1631+
1632+
/**
1633+
* Set the xMsOriginalContentLength property: The wire size of the encoded structured message body before decoding.
1634+
*
1635+
* @param xMsOriginalContentLength the xMsOriginalContentLength value to set.
1636+
* @return the BlobsDownloadHeaders object itself.
1637+
*/
1638+
public BlobsDownloadHeaders setXMsOriginalContentLength(Long xMsOriginalContentLength) {
1639+
this.xMsOriginalContentLength = xMsOriginalContentLength;
1640+
return this;
1641+
}
16081642
}

sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,4 +1044,25 @@ public BlobDownloadHeaders setCreationTime(OffsetDateTime creationTime) {
10441044
internalHeaders.setXMsCreationTime(creationTime);
10451045
return this;
10461046
}
1047+
1048+
/**
1049+
* Get the originalContentLength property: The wire size of the encoded structured message body before decoding.
1050+
* Only present when content validation is active and the response was decoded transparently.
1051+
*
1052+
* @return the originalContentLength value.
1053+
*/
1054+
public Long getOriginalContentLength() {
1055+
return internalHeaders.getXMsOriginalContentLength();
1056+
}
1057+
1058+
/**
1059+
* Set the originalContentLength property: The wire size of the encoded structured message body before decoding.
1060+
*
1061+
* @param originalContentLength the originalContentLength value to set.
1062+
* @return the BlobDownloadHeaders object itself.
1063+
*/
1064+
public BlobDownloadHeaders setOriginalContentLength(Long originalContentLength) {
1065+
internalHeaders.setXMsOriginalContentLength(originalContentLength);
1066+
return this;
1067+
}
10471068
}

sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ public static final class HeaderConstants {
264264
public static final HttpHeaderName STRUCTURED_CONTENT_LENGTH_HEADER_NAME
265265
= HttpHeaderName.fromString(STRUCTURED_CONTENT_LENGTH);
266266

267+
public static final String ORIGINAL_CONTENT_LENGTH = "x-ms-original-content-length";
268+
269+
public static final HttpHeaderName ORIGINAL_CONTENT_LENGTH_HEADER_NAME
270+
= HttpHeaderName.fromString(ORIGINAL_CONTENT_LENGTH);
271+
267272
/**
268273
* Metadata key ("hdi_isfolder") used to mark virtual directories in Azure Blob Storage.
269274
*

sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/DecodedResponse.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.azure.core.http.HttpResponse;
99
import com.azure.core.util.CoreUtils;
1010
import com.azure.core.util.FluxUtil;
11+
import com.azure.storage.common.implementation.Constants;
1112
import reactor.core.publisher.Flux;
1213
import reactor.core.publisher.Mono;
1314

@@ -26,19 +27,30 @@
2627
class DecodedResponse extends HttpResponse {
2728
private final HttpResponse originalResponse;
2829
private final Flux<ByteBuffer> decodedBody;
30+
private final HttpHeaders adjustedHeaders;
2931

3032
/**
3133
* Wraps {@code httpResponse} with a body backed by {@code decodedBody}.
3234
*
33-
* @param httpResponse The original response from the storage service. Its request, status code, and headers
34-
* are preserved verbatim.
35-
* @param decodedBody The Flux of CRC-validated, framing-stripped payload bytes produced by the decoder
36-
* pipeline.
35+
* <p>{@code Content-Length} is overridden to {@code decodedContentLength} so callers see the size of the bytes
36+
* they will actually read. The original wire size is preserved in {@code x-ms-original-content-length}.</p>
37+
*
38+
* @param httpResponse The original response from the storage service.
39+
* @param decodedBody The Flux of CRC-validated, framing-stripped payload bytes produced by the decoder pipeline.
40+
* @param originalContentLength The wire size of the encoded structured message body.
41+
* @param decodedContentLength The size of the decoded payload that callers will consume.
3742
*/
38-
DecodedResponse(HttpResponse httpResponse, Flux<ByteBuffer> decodedBody) {
43+
DecodedResponse(HttpResponse httpResponse, Flux<ByteBuffer> decodedBody, long originalContentLength,
44+
long decodedContentLength) {
3945
super(httpResponse.getRequest());
4046
this.originalResponse = httpResponse;
4147
this.decodedBody = decodedBody;
48+
HttpHeaders headers = new HttpHeaders();
49+
httpResponse.getHeaders().stream().forEach(h -> headers.set(h.getName(), h.getValue()));
50+
headers.set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(decodedContentLength));
51+
headers.set(Constants.HeaderConstants.ORIGINAL_CONTENT_LENGTH_HEADER_NAME,
52+
String.valueOf(originalContentLength));
53+
this.adjustedHeaders = headers;
4254
}
4355

4456
@Override
@@ -49,12 +61,12 @@ public int getStatusCode() {
4961
@Override
5062
@SuppressWarnings("deprecation")
5163
public String getHeaderValue(String name) {
52-
return originalResponse.getHeaderValue(name);
64+
return adjustedHeaders.getValue(name);
5365
}
5466

5567
@Override
5668
public HttpHeaders getHeaders() {
57-
return originalResponse.getHeaders();
69+
return adjustedHeaders;
5870
}
5971

6072
@Override

sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/policy/StorageContentValidationDecoderPolicy.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,14 @@ public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineN
7373
// Confirm the service actually honored our structured-body request before we hand the body to the decoder.
7474
validateStructuredMessageHeaders(httpResponse);
7575

76+
Long decodedContentLength = getStructuredContentLength(httpResponse.getHeaders());
77+
7678
// Fresh decoder per response so retries each get a clean state machine.
7779
StructuredMessageDecoder decoder = new StructuredMessageDecoder(contentLength);
7880

7981
Flux<ByteBuffer> decodedStream = decodeStream(httpResponse.getBody(), decoder);
80-
return new DecodedResponse(httpResponse, decodedStream);
82+
// decodedContentLength is guaranteed non-null here: validateStructuredMessageHeaders confirmed its presence.
83+
return new DecodedResponse(httpResponse, decodedStream, contentLength, decodedContentLength);
8184
});
8285
}
8386

@@ -123,6 +126,22 @@ private static Long getContentLength(HttpHeaders headers) {
123126
return null;
124127
}
125128

129+
/**
130+
* Reads {@code x-ms-structured-content-length} as a {@code Long}, returning {@code null} if missing or
131+
* unparseable.
132+
*/
133+
private static Long getStructuredContentLength(HttpHeaders headers) {
134+
String value = headers.getValue(Constants.HeaderConstants.STRUCTURED_CONTENT_LENGTH_HEADER_NAME);
135+
if (value != null) {
136+
try {
137+
return Long.parseLong(value);
138+
} catch (NumberFormatException e) {
139+
// Malformed header; fall through to null.
140+
}
141+
}
142+
return null;
143+
}
144+
126145
/**
127146
* @return true for a 2xx response to a GET request, the only response shape that carries a body we
128147
* can decode. 206 (Partial Content) on retried range downloads is included.

sdk/storage/azure-storage-common/src/test/java/com/azure/storage/common/policy/DecodedResponseTests.java

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.azure.core.http.HttpResponse;
1111
import com.azure.core.test.http.MockHttpResponse;
1212
import com.azure.core.util.BinaryData;
13+
import com.azure.storage.common.implementation.Constants;
1314
import org.junit.jupiter.api.Test;
1415
import reactor.core.publisher.Flux;
1516
import reactor.test.StepVerifier;
@@ -64,17 +65,19 @@ public void preservesRequestStatusCodeAndHeaders() {
6465
HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, "100").set(CUSTOM_HEADER, "value");
6566
MockHttpResponse original = mockResponse(206, h, bytes("encoded"));
6667

67-
DecodedResponse wrapper = new DecodedResponse(original, fluxOf(bytes("decoded")));
68+
DecodedResponse wrapper = new DecodedResponse(original, fluxOf(bytes("decoded")), 100L, 80L);
6869

6970
assertSame(original.getRequest(), wrapper.getRequest());
7071
assertEquals(206, wrapper.getStatusCode());
71-
assertSame(h, wrapper.getHeaders());
72+
// Content-Length is overridden to decoded size; other headers are preserved.
73+
assertEquals("80", wrapper.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
74+
assertEquals("value", wrapper.getHeaders().getValue(CUSTOM_HEADER));
7275
}
7376

7477
@Test
7578
public void getHeaderValueByStringReturnsHeaderValue() {
7679
HttpHeaders h = headers(CUSTOM_HEADER, "value");
77-
DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]));
80+
DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]), 0L, 0L);
7881

7982
assertEquals("value", wrapper.getHeaderValue(CUSTOM_HEADER.getCaseInsensitiveName()));
8083
assertNull(wrapper.getHeaderValue("nonexistent"));
@@ -84,7 +87,7 @@ public void getHeaderValueByStringReturnsHeaderValue() {
8487
public void getBodyReturnsDecodedFlux() {
8588
byte[] decoded = bytes("decoded body");
8689
DecodedResponse wrapper
87-
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
90+
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
8891

8992
StepVerifier.create(wrapper.getBody().reduce(new ByteArrayOutputStream(), (sink, buf) -> {
9093
byte[] copy = new byte[buf.remaining()];
@@ -98,7 +101,7 @@ public void getBodyReturnsDecodedFlux() {
98101
public void getBodyAsByteArrayReturnsDecodedBytes() {
99102
byte[] decoded = bytes("decoded body");
100103
DecodedResponse wrapper
101-
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
104+
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
102105

103106
StepVerifier.create(wrapper.getBodyAsByteArray())
104107
.expectNextMatches(b -> Arrays.equals(decoded, b))
@@ -111,7 +114,7 @@ public void getBodyAsStringDefaultsToUtf8WhenNoCharsetSpecified() {
111114
// BOM nor a Content-Type charset parameter is present. This test pins the "no headers, no BOM" path.
112115
String text = "héllo wörld – ✓";
113116
DecodedResponse wrapper = new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]),
114-
fluxOf(text.getBytes(StandardCharsets.UTF_8)));
117+
fluxOf(text.getBytes(StandardCharsets.UTF_8)), 0L, 0L);
115118

116119
StepVerifier.create(wrapper.getBodyAsString()).expectNext(text).verifyComplete();
117120
}
@@ -124,7 +127,7 @@ public void getBodyAsStringHonorsCharsetFromContentTypeHeader() {
124127
String text = "ümlaut";
125128
byte[] iso = text.getBytes(StandardCharsets.ISO_8859_1);
126129
HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, "text/plain; charset=ISO-8859-1");
127-
DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(iso));
130+
DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(iso), 0L, 0L);
128131

129132
StepVerifier.create(wrapper.getBodyAsString()).expectNext(text).verifyComplete();
130133
}
@@ -140,7 +143,7 @@ public void getBodyAsStringDetectsUtf8BomAndStripsIt() {
140143
System.arraycopy(bom, 0, withBom, 0, bom.length);
141144
System.arraycopy(payload, 0, withBom, bom.length, payload.length);
142145
DecodedResponse wrapper
143-
= new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(withBom));
146+
= new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(withBom), 0L, 0L);
144147

145148
StepVerifier.create(wrapper.getBodyAsString()).expectNext(text).verifyComplete();
146149
}
@@ -150,7 +153,7 @@ public void getBodyAsStringDecodesUsingProvidedCharset() {
150153
String text = "ümlaut";
151154
byte[] latin1 = text.getBytes(StandardCharsets.ISO_8859_1);
152155
DecodedResponse wrapper
153-
= new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(latin1));
156+
= new DecodedResponse(mockResponse(200, new HttpHeaders(), new byte[0]), fluxOf(latin1), 0L, 0L);
154157

155158
StepVerifier.create(wrapper.getBodyAsString(StandardCharsets.ISO_8859_1)).expectNext(text).verifyComplete();
156159
}
@@ -160,7 +163,7 @@ public void inheritedGetBodyAsInputStreamUsesDecodedBytes() throws IOException {
160163
// Base getBodyAsInputStream() routes through getBodyAsByteArray(), so the override is exercised end-to-end.
161164
byte[] decoded = bytes("decoded stream");
162165
DecodedResponse wrapper
163-
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
166+
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
164167

165168
try (InputStream stream = wrapper.getBodyAsInputStream().block()) {
166169
assertNotNull(stream);
@@ -172,7 +175,7 @@ public void inheritedGetBodyAsInputStreamUsesDecodedBytes() throws IOException {
172175
public void inheritedWriteBodyToWritesDecodedBytes() throws IOException {
173176
byte[] decoded = bytes("write me");
174177
DecodedResponse wrapper
175-
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
178+
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
176179

177180
ByteArrayOutputStream sink = new ByteArrayOutputStream();
178181
try (WritableByteChannel channel = Channels.newChannel(sink)) {
@@ -186,7 +189,7 @@ public void inheritedWriteBodyToWritesDecodedBytes() throws IOException {
186189
public void inheritedBufferReturnsResponseBackedByDecodedBytes() {
187190
byte[] decoded = bytes("buffered");
188191
DecodedResponse wrapper
189-
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded));
192+
= new DecodedResponse(mockResponse(200, new HttpHeaders(), bytes("encoded")), fluxOf(decoded), 0L, 0L);
190193

191194
HttpResponse buffered = wrapper.buffer();
192195
assertNotNull(buffered);
@@ -201,15 +204,34 @@ public void inheritedGetBodyAsBinaryDataReturnsDecodedBytes() {
201204
// must contain the decoded payload, not the original wire body. A divergent Content-Length header is set
202205
// to make the wire vs decoded distinction explicit and guard against regressions in header forwarding.
203206
byte[] decoded = bytes("decoded payload");
204-
HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(decoded.length + 32));
205-
DecodedResponse wrapper
206-
= new DecodedResponse(mockResponse(200, h, bytes("encoded wire body")), fluxOf(decoded));
207+
long originalSize = decoded.length + 32;
208+
HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(originalSize));
209+
DecodedResponse wrapper = new DecodedResponse(mockResponse(200, h, bytes("encoded wire body")), fluxOf(decoded),
210+
originalSize, decoded.length);
207211

208212
BinaryData data = wrapper.getBodyAsBinaryData();
209213
assertNotNull(data);
210214
assertArrayEquals(decoded, data.toBytes());
211215
}
212216

217+
@Test
218+
public void contentLengthIsOverriddenToDecodedSizeAndOriginalIsPreserved() {
219+
long originalSize = 500L;
220+
long decodedSize = 300L;
221+
HttpHeaders h = new HttpHeaders().set(HttpHeaderName.CONTENT_LENGTH, String.valueOf(originalSize))
222+
.set(CUSTOM_HEADER, "preserve-me");
223+
DecodedResponse wrapper
224+
= new DecodedResponse(mockResponse(200, h, new byte[0]), fluxOf(new byte[0]), originalSize, decodedSize);
225+
226+
assertEquals(String.valueOf(decodedSize), wrapper.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH));
227+
assertEquals(String.valueOf(originalSize),
228+
wrapper.getHeaders().getValue(Constants.HeaderConstants.ORIGINAL_CONTENT_LENGTH_HEADER_NAME));
229+
assertEquals("preserve-me", wrapper.getHeaders().getValue(CUSTOM_HEADER));
230+
// Deprecated getHeaderValue must reflect the same overrides.
231+
assertEquals(String.valueOf(decodedSize),
232+
wrapper.getHeaderValue(HttpHeaderName.CONTENT_LENGTH.getCaseInsensitiveName()));
233+
}
234+
213235
private static byte[] readAll(InputStream stream) throws IOException {
214236
ByteArrayOutputStream out = new ByteArrayOutputStream();
215237
byte[] buf = new byte[1024];

0 commit comments

Comments
 (0)