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/4] Initial plan From 3b70576512e31af1f73f50255f8f3f7862a2403b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:41:51 +0000 Subject: [PATCH 2/4] Fix missing blob If-Match reads Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- ChangeLog.md | 1 + src/blob/persistence/LokiBlobMetadataStore.ts | 8 ++-- src/blob/persistence/SqlBlobMetadataStore.ts | 20 ++-------- tests/blob/apis/blob.test.ts | 40 +++++++++++++++++++ 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 71f381dd4..3b8a6c0d6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -13,6 +13,7 @@ General: Blob: +- Fixed Blob GET and HEAD requests for nonexistent blobs with `If-Match` returning `BlobNotFound` instead of `ConditionNotMet`, matching Azure Storage. (issue #2751) - 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/persistence/LokiBlobMetadataStore.ts b/src/blob/persistence/LokiBlobMetadataStore.ts index 20715a2d4..615c907bc 100644 --- a/src/blob/persistence/LokiBlobMetadataStore.ts +++ b/src/blob/persistence/LokiBlobMetadataStore.ts @@ -1206,12 +1206,12 @@ export default class LokiBlobMetadataStore true ); - validateReadConditions(context, modifiedAccessConditions, doc); - if (!doc) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } + validateReadConditions(context, modifiedAccessConditions, doc); + new BlobReadLeaseValidator(leaseAccessConditions).validate( new BlobLeaseAdapter(doc), context @@ -1289,13 +1289,13 @@ export default class LokiBlobMetadataStore true ); - validateReadConditions(context, modifiedAccessConditions, doc); - // When block blob don't have committed block, should return 404 if (!doc) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } + validateReadConditions(context, modifiedAccessConditions, doc); + new BlobReadLeaseValidator(leaseAccessConditions).validate( new BlobLeaseAdapter(doc), context diff --git a/src/blob/persistence/SqlBlobMetadataStore.ts b/src/blob/persistence/SqlBlobMetadataStore.ts index a3da94979..126fd1f52 100644 --- a/src/blob/persistence/SqlBlobMetadataStore.ts +++ b/src/blob/persistence/SqlBlobMetadataStore.ts @@ -1188,14 +1188,6 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); - validateReadConditions( - context, - modifiedAccessConditions, - blobFindResult - ? this.convertDbModelToBlobModel(blobFindResult) - : undefined - ); - if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -1204,6 +1196,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { blobFindResult ); + validateReadConditions(context, modifiedAccessConditions, blobModel); + return LeaseFactory.createLeaseState( new BlobLeaseAdapter(blobModel), context @@ -1790,14 +1784,6 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { transaction: t }); - validateReadConditions( - context, - modifiedAccessConditions, - blobFindResult - ? this.convertDbModelToBlobModel(blobFindResult) - : undefined - ); - if (blobFindResult === null || blobFindResult === undefined) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } @@ -1806,6 +1792,8 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { blobFindResult ); + validateReadConditions(context, modifiedAccessConditions, blobModel); + if (!blobModel.isCommitted) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } diff --git a/tests/blob/apis/blob.test.ts b/tests/blob/apis/blob.test.ts index fcc52a712..b74cd822e 100644 --- a/tests/blob/apis/blob.test.ts +++ b/tests/blob/apis/blob.test.ts @@ -214,6 +214,46 @@ describe("BlobAPIs", () => { assert.fail(); }); + it("download should return BlobNotFound for nonexistent blob with ifMatch @loki @sql", async () => { + const properties = await blobClient.getProperties(); + const nonExistentBlobClient = containerClient.getBlobClient( + getUniqueName("missingblob") + ); + + try { + await nonExistentBlobClient.download(0, undefined, { + conditions: { + ifMatch: properties.etag + } + }); + } catch (error) { + assert.deepStrictEqual((error as any).statusCode, 404); + assert.deepStrictEqual((error as any).details.errorCode, "BlobNotFound"); + return; + } + assert.fail(); + }); + + it("getProperties should return BlobNotFound for nonexistent blob with ifMatch @loki @sql", async () => { + const properties = await blobClient.getProperties(); + const nonExistentBlobClient = containerClient.getBlobClient( + getUniqueName("missingblob") + ); + + try { + await nonExistentBlobClient.getProperties({ + conditions: { + ifMatch: properties.etag + } + }); + } catch (error) { + assert.deepStrictEqual((error as any).statusCode, 404); + assert.deepStrictEqual((error as any).details.errorCode, "BlobNotFound"); + return; + } + assert.fail(); + }); + it("download should not work with conditional header ifNoneMatch @loki @sql", async () => { const properties = await blobClient.getProperties(); try { From 94670371b771d5c6a62016116165d50eeee51b7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:43:46 +0000 Subject: [PATCH 3/4] Clarify changelog wording Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 3b8a6c0d6..090b3e4b5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -13,7 +13,7 @@ General: Blob: -- Fixed Blob GET and HEAD requests for nonexistent blobs with `If-Match` returning `BlobNotFound` instead of `ConditionNotMet`, matching Azure Storage. (issue #2751) +- 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) - 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) From 2ed5f85b5f9b9723a9c7471d9249b3a7d2d49ed2 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:34 +0000 Subject: [PATCH 4/4] Preserve SQL get-properties not-found precedence Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- src/blob/persistence/SqlBlobMetadataStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blob/persistence/SqlBlobMetadataStore.ts b/src/blob/persistence/SqlBlobMetadataStore.ts index 126fd1f52..beb0a2f9d 100644 --- a/src/blob/persistence/SqlBlobMetadataStore.ts +++ b/src/blob/persistence/SqlBlobMetadataStore.ts @@ -1792,12 +1792,12 @@ export default class SqlBlobMetadataStore implements IBlobMetadataStore { blobFindResult ); - validateReadConditions(context, modifiedAccessConditions, blobModel); - if (!blobModel.isCommitted) { throw StorageErrorFactory.getBlobNotFound(context.contextId); } + validateReadConditions(context, modifiedAccessConditions, blobModel); + // TODO: Return blobCommittedBlockCount for append blob let responds = LeaseFactory.createLeaseState(