From 1181951122bb80bfd27752a3d78b7c626145c663 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:38:04 +0000 Subject: [PATCH 1/5] Initial plan From abe29d10ccbc44642c8c1ade6267e842e1729a0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:42:02 +0000 Subject: [PATCH 2/5] Fix stage block content MD5 response Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- ChangeLog.md | 1 + src/blob/handlers/BlockBlobHandler.ts | 11 +++++++++-- tests/blob/apis/blockblob.test.ts | 6 ++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 71f381dd4..0c09d20c1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -13,6 +13,7 @@ General: Blob: +- Fixed Stage Block responses for API versions after `2019-02-02` to return `Content-MD5` when the request supplies `Content-MD5`, matching Azure Storage while preserving CRC64 responses for requests without MD5. (issue #2394) - Fixed block blob uploads with `If-None-Match: *` returning `BlobAlreadyExists` before validating an active lease, matching Azure Storage's `LeaseIdMissing` and lease mismatch error precedence. (issue #2637) - Fixed service- and container-level Filter Blobs requests failing when the optional `where` query parameter is omitted. - Fixed blob operations hanging when a client disconnects before the operation queue processes the request. (issue #2575) diff --git a/src/blob/handlers/BlockBlobHandler.ts b/src/blob/handlers/BlockBlobHandler.ts index e03c09e5e..9cf74a04c 100644 --- a/src/blob/handlers/BlockBlobHandler.ts +++ b/src/blob/handlers/BlockBlobHandler.ts @@ -485,13 +485,20 @@ export default class BlockBlobHandler persistency, context.contextId ); - const { crc64: calculatedCRC64 } = + const { md5: calculatedContentMD5, crc64: calculatedCRC64 } = await computeAndValidateTransactionalChecksums( stream, { md5: contentMD5, crc64: contentCRC64 }, context.contextId, { crc64: contentMD5 === undefined } ); + const requestApiVersion = context.request!.getHeader( + HeaderConstants.X_MS_VERSION + ); + const shouldReturnContentMD5 = + contentMD5 !== undefined && + requestApiVersion !== undefined && + requestApiVersion > "2019-02-02"; const block: BlockModel = { accountName, @@ -512,7 +519,7 @@ export default class BlockBlobHandler const response: Models.BlockBlobStageBlockResponse = { statusCode: 201, - contentMD5: undefined, // TODO: Block content MD5 + contentMD5: shouldReturnContentMD5 ? calculatedContentMD5 : undefined, xMsContentCrc64: calculatedCRC64, requestId: blobCtx.contextId, version: BLOB_API_VERSION, diff --git a/tests/blob/apis/blockblob.test.ts b/tests/blob/apis/blockblob.test.ts index b9b04ee18..d7648381b 100644 --- a/tests/blob/apis/blockblob.test.ts +++ b/tests/blob/apis/blockblob.test.ts @@ -1958,19 +1958,21 @@ describe("BlockBlobAPIs", () => { assert.fail("Did not throw an exception."); }); - it("stageBlock with md5 hash check @loki @sql", async () => { + it("stageBlock with md5 hash check returns Content-MD5 after 2019-02-02 @loki @sql", async () => { const body = "HelloWorld"; const md5 = crypto.createHash("md5").update(body, "utf8").digest(); const options = { transactionalContentMD5: new Uint8Array(md5) }; - await blockBlobClient.stageBlock( + const result = await blockBlobClient.stageBlock( base64encode("1"), body, body.length, options ); + assert.deepStrictEqual(Buffer.from(result.contentMD5!), md5); + assert.equal(result.xMsContentCrc64, undefined); const listResponse = await blockBlobClient.getBlockList("uncommitted"); assert.equal(listResponse.uncommittedBlocks!.length, 1); From 056c613980e3e37be61fb22c3829ae9793f99863 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:44:01 +0000 Subject: [PATCH 3/5] Address stage block MD5 review feedback Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- src/blob/handlers/BlockBlobHandler.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/blob/handlers/BlockBlobHandler.ts b/src/blob/handlers/BlockBlobHandler.ts index 9cf74a04c..6c21c20ad 100644 --- a/src/blob/handlers/BlockBlobHandler.ts +++ b/src/blob/handlers/BlockBlobHandler.ts @@ -26,6 +26,12 @@ import { validateTransactionalChecksumHeaders } from "../utils/utils"; +const STAGE_BLOCK_CONTENT_MD5_RESPONSE_API_VERSION = "2019-02-02"; + +function isApiVersionAfter(apiVersion: string, baselineVersion: string): boolean { + return apiVersion > baselineVersion; +} + /** * Agents for the loopback self-request stageBlockFromURL makes to read a copy * source, keyed by the certificate they pin. Shared so requests reuse one @@ -494,11 +500,13 @@ export default class BlockBlobHandler ); const requestApiVersion = context.request!.getHeader( HeaderConstants.X_MS_VERSION - ); + ) || BLOB_API_VERSION; const shouldReturnContentMD5 = contentMD5 !== undefined && - requestApiVersion !== undefined && - requestApiVersion > "2019-02-02"; + isApiVersionAfter( + requestApiVersion, + STAGE_BLOCK_CONTENT_MD5_RESPONSE_API_VERSION + ); const block: BlockModel = { accountName, From 93c86dfde79284305b0755830965157a39534e03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:45:56 +0000 Subject: [PATCH 4/5] Cover stage block MD5 version gate Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- src/blob/handlers/BlockBlobHandler.ts | 3 +++ tests/blob/apis/blockblob.test.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/blob/handlers/BlockBlobHandler.ts b/src/blob/handlers/BlockBlobHandler.ts index 6c21c20ad..29f7f1104 100644 --- a/src/blob/handlers/BlockBlobHandler.ts +++ b/src/blob/handlers/BlockBlobHandler.ts @@ -28,6 +28,9 @@ import { const STAGE_BLOCK_CONTENT_MD5_RESPONSE_API_VERSION = "2019-02-02"; +// Azure Storage API versions are zero-padded YYYY-MM-DD strings, so +// lexicographic comparison matches chronological order. Non-date versions are +// not supported here. function isApiVersionAfter(apiVersion: string, baselineVersion: string): boolean { return apiVersion > baselineVersion; } diff --git a/tests/blob/apis/blockblob.test.ts b/tests/blob/apis/blockblob.test.ts index d7648381b..a7b91241f 100644 --- a/tests/blob/apis/blockblob.test.ts +++ b/tests/blob/apis/blockblob.test.ts @@ -1980,6 +1980,25 @@ describe("BlockBlobAPIs", () => { assert.equal(listResponse.uncommittedBlocks![0].size, body.length); }); + it("stageBlock with md5 hash check omits Content-MD5 for 2019-02-02 @loki @sql", async () => { + const body = "HelloWorld"; + const md5 = crypto.createHash("md5").update(body, "utf8").digest(); + const oldVersionClient = getBlockBlobClientWithRawHeaders( + containerName, + blobName, + [{ key: "x-ms-version", value: "2019-02-02" }] + ); + + const result = await oldVersionClient.stageBlock( + base64encode("1"), + body, + body.length, + { transactionalContentMD5: new Uint8Array(md5) } + ); + assert.equal(result.contentMD5, undefined); + assert.equal(result.xMsContentCrc64, undefined); + }); + it("stageBlock with correct crc64 should succeed @loki @sql", async () => { const body = "HelloWorld"; const crc64 = getCRC64FromString(body); From bb8d995dfd674342ce2950c35f4400a4349686f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:47:28 +0000 Subject: [PATCH 5/5] Inline stage block version comparison Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- src/blob/handlers/BlockBlobHandler.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/blob/handlers/BlockBlobHandler.ts b/src/blob/handlers/BlockBlobHandler.ts index 29f7f1104..26a3634e3 100644 --- a/src/blob/handlers/BlockBlobHandler.ts +++ b/src/blob/handlers/BlockBlobHandler.ts @@ -28,13 +28,6 @@ import { const STAGE_BLOCK_CONTENT_MD5_RESPONSE_API_VERSION = "2019-02-02"; -// Azure Storage API versions are zero-padded YYYY-MM-DD strings, so -// lexicographic comparison matches chronological order. Non-date versions are -// not supported here. -function isApiVersionAfter(apiVersion: string, baselineVersion: string): boolean { - return apiVersion > baselineVersion; -} - /** * Agents for the loopback self-request stageBlockFromURL makes to read a copy * source, keyed by the certificate they pin. Shared so requests reuse one @@ -504,12 +497,12 @@ export default class BlockBlobHandler const requestApiVersion = context.request!.getHeader( HeaderConstants.X_MS_VERSION ) || BLOB_API_VERSION; + // Blob API versions are validated zero-padded YYYY-MM-DD strings here, so + // lexicographic comparison matches chronological order. + const isAfterContentMD5ResponseVersion = + requestApiVersion > STAGE_BLOCK_CONTENT_MD5_RESPONSE_API_VERSION; const shouldReturnContentMD5 = - contentMD5 !== undefined && - isApiVersionAfter( - requestApiVersion, - STAGE_BLOCK_CONTENT_MD5_RESPONSE_API_VERSION - ); + contentMD5 !== undefined && isAfterContentMD5ResponseVersion; const block: BlockModel = { accountName,