diff --git a/ChangeLog.md b/ChangeLog.md index 60d3f186b..e2abedcf7 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -15,6 +15,7 @@ General: Blob: +- Fixed Stage Block responses for API version `2019-02-02` and later to return `Content-MD5` when the request supplies `Content-MD5`, matching Azure Storage while preserving CRC64 responses for requests without MD5. (issue #2394) - Bumped the default Blob service API version to `2026-10-06`, and added `2026-10-06` to the supported Blob service API versions. - Fixed Blob GET and HEAD requests for nonexistent blobs with `If-Match` so they now return `BlobNotFound` instead of `ConditionNotMet`, matching Azure Storage. (issue #2751) - Added opt-in, per-account Blob Versioning configured with `--accountConfigFilePath` or `--accountConfigAsJson`; Azurite preserves previous versions on supported blob writes and supports listing, reading, restoring, and deleting specific versions. diff --git a/src/blob/handlers/BlockBlobHandler.ts b/src/blob/handlers/BlockBlobHandler.ts index ac5dc2155..43d28fd0d 100644 --- a/src/blob/handlers/BlockBlobHandler.ts +++ b/src/blob/handlers/BlockBlobHandler.ts @@ -487,13 +487,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 + ) || BLOB_API_VERSION; + // Blob API versions are validated zero-padded YYYY-MM-DD strings here, so + // lexicographic comparison matches chronological order. + const shouldReturnContentMD5 = + contentMD5 !== undefined && requestApiVersion >= "2019-02-02"; const block: BlockModel = { accountName, @@ -514,7 +521,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 303d2c73b..348e568d8 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 returns 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.deepStrictEqual(Buffer.from(result.contentMD5!), md5); + assert.equal(result.xMsContentCrc64, undefined); + }); + it("stageBlock with correct crc64 should succeed @loki @sql", async () => { const body = "HelloWorld"; const crc64 = getCRC64FromString(body);