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..26a3634e3 100644 --- a/src/blob/handlers/BlockBlobHandler.ts +++ b/src/blob/handlers/BlockBlobHandler.ts @@ -26,6 +26,8 @@ import { validateTransactionalChecksumHeaders } from "../utils/utils"; +const STAGE_BLOCK_CONTENT_MD5_RESPONSE_API_VERSION = "2019-02-02"; + /** * Agents for the loopback self-request stageBlockFromURL makes to read a copy * source, keyed by the certificate they pin. Shared so requests reuse one @@ -485,13 +487,22 @@ 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 + ) || 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 && isAfterContentMD5ResponseVersion; const block: BlockModel = { accountName, @@ -512,7 +523,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..a7b91241f 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); @@ -1978,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);