From 452169d90cf84585f4d288f5dad1b91947ba89bd Mon Sep 17 00:00:00 2001 From: Quetzal Bradley Date: Tue, 17 Mar 2026 13:02:40 -0700 Subject: [PATCH 1/3] Fix FilterBlobs crash when where clause is omitted Service_FilterBlobs and Container_FilterBlobs crashed with HTTP 500 when the optional 'where' query parameter was not provided. The response object used 'options.where!' (non-null assertion) which set the required 'where' field to undefined, causing serialization to fail. Replace 'options.where!' with 'options.where || ""' so that a missing where clause produces an empty string instead of undefined. Both handlers now return 200 with an empty blob list when no filter expression is given. Add regression test that sends a raw HTTP FilterBlobs request without a where parameter and asserts both service-level and container-level endpoints return 200. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/blob/handlers/ContainerHandler.ts | 2 +- src/blob/handlers/ServiceHandler.ts | 2 +- tests/blob/apis/service.test.ts | 127 ++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) diff --git a/src/blob/handlers/ContainerHandler.ts b/src/blob/handlers/ContainerHandler.ts index 66c40af6d..9b091abd1 100644 --- a/src/blob/handlers/ContainerHandler.ts +++ b/src/blob/handlers/ContainerHandler.ts @@ -403,7 +403,7 @@ export default class ContainerHandler extends BaseHandler version: BLOB_API_VERSION, date: context.startTime, serviceEndpoint, - where: options.where!, + where: options.where || "", blobs: blobs, clientRequestId: options.requestId, nextMarker: `${nextMarker || ""}` diff --git a/src/blob/handlers/ServiceHandler.ts b/src/blob/handlers/ServiceHandler.ts index c7cb5010e..da54e48fb 100644 --- a/src/blob/handlers/ServiceHandler.ts +++ b/src/blob/handlers/ServiceHandler.ts @@ -406,7 +406,7 @@ export default class ServiceHandler extends BaseHandler version: BLOB_API_VERSION, date: context.startTime, serviceEndpoint, - where: options.where!, + where: options.where || "", blobs: blobs, clientRequestId: options.requestId, nextMarker: `${nextMarker || ""}` diff --git a/tests/blob/apis/service.test.ts b/tests/blob/apis/service.test.ts index cf9e94850..e36844f50 100644 --- a/tests/blob/apis/service.test.ts +++ b/tests/blob/apis/service.test.ts @@ -826,6 +826,133 @@ describe("ServiceAPIs", () => { await containerClient.delete(); }); + + it("filterBlobs without where clause should return empty results @loki @sql", async function () { + // Regression test: calling FilterBlobs (GET /?comp=blobs) without a + // 'where' query parameter used to crash with HTTP 500 because + // options.where was undefined and the response constructor used the + // non-null assertion operator on it. After the fix both + // Service_FilterBlobs and Container_FilterBlobs should return 200 + // with an empty blob list. + const http = require("http"); + const crypto = require("crypto"); + + const host = server.config.host; + const port = server.config.port; + const account = "devstoreaccount1"; + const accountKey = + "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; + + function signAndSend( + method: string, + urlPath: string + ): Promise<{ status: number; body: string }> { + return new Promise((resolve, reject) => { + const now = new Date().toUTCString(); + const headers: Record = { + "x-ms-date": now, + "x-ms-version": "2021-10-04" + }; + + // Build canonicalized headers + const canonHeaders = Object.keys(headers) + .filter((k) => k.startsWith("x-ms-")) + .sort() + .map((k) => `${k}:${headers[k]}\n`) + .join(""); + + // Build canonicalized resource (emulator doubles the account name) + const [pathPart, queryPart] = urlPath.split("?"); + let canonResource = `/${account}/${account}${pathPart}`; + if (queryPart) { + const params: Record = {}; + for (const seg of queryPart.split("&")) { + const [k, v] = seg.split("="); + (params[k.toLowerCase()] = params[k.toLowerCase()] || []).push( + v || "" + ); + } + for (const k of Object.keys(params).sort()) { + canonResource += `\n${k}:${params[k].sort().join(",")}`; + } + } + + const stringToSign = [ + method, + "", // Content-Encoding + "", // Content-Language + "", // Content-Length + "", // Content-MD5 + "", // Content-Type + "", // Date + "", // If-Modified-Since + "", // If-Match + "", // If-None-Match + "", // If-Unmodified-Since + "", // Range + canonHeaders + canonResource + ].join("\n"); + + const sig = crypto + .createHmac("sha256", Buffer.from(accountKey, "base64")) + .update(stringToSign, "utf8") + .digest("base64"); + headers["Authorization"] = `SharedKey ${account}:${sig}`; + + const req = http.request( + { + hostname: host, + port, + path: `/${account}${urlPath}`, + method, + headers + }, + (res: any) => { + let body = ""; + res.on("data", (chunk: any) => (body += chunk)); + res.on("end", () => + resolve({ status: res.statusCode, body }) + ); + } + ); + req.on("error", reject); + req.end(); + }); + } + + // Service-level FilterBlobs without 'where' — should return 200 + const serviceResult = await signAndSend("GET", "/?comp=blobs"); + assert.strictEqual( + serviceResult.status, + 200, + `Service_FilterBlobs without where should return 200, got ${serviceResult.status}: ${serviceResult.body}` + ); + assert.ok( + serviceResult.body.includes(" element" + ); + + // Container-level FilterBlobs without 'where' — create a container first + const containerName = getUniqueName("filtertest"); + const containerClient = serviceClient.getContainerClient(containerName); + await containerClient.create(); + + const containerResult = await signAndSend( + "GET", + `/${containerName}?restype=container&comp=blobs` + ); + assert.strictEqual( + containerResult.status, + 200, + `Container_FilterBlobs without where should return 200, got ${containerResult.status}: ${containerResult.body}` + ); + assert.ok( + containerResult.body.includes(" element" + ); + + await containerClient.delete(); + }); }); describe("ServiceAPIs - secondary location endpoint", () => { From ba35e389537ecfc0b5eb508bd7b3c6f5176b8717 Mon Sep 17 00:00:00 2001 From: Quetzal Bradley Date: Tue, 17 Mar 2026 13:50:29 -0700 Subject: [PATCH 2/3] Add ChangeLog.md entry --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index 51cd48fd2..9989aa603 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,6 +9,7 @@ General: - Performance improvements for internal metadata access using in-memory metadata store - Fix building failure on Node 22 platform. - Fix * IfMatch for non-existent resource not throwing 412 Precondition Failed +- Fix FilterBlobs returning 500 instead of 200 with optional missing where parameter. ## 2025.07 Version 3.35.0 From 7bccd0a5cc05d390ab9a6e956c1d17e6327129c6 Mon Sep 17 00:00:00 2001 From: Quetzal Bradley Date: Tue, 17 Mar 2026 13:58:32 -0700 Subject: [PATCH 3/3] Use EMULATOR_ACCOUNT_NAME/KEY constants in filterBlobs test Replace inline account name and key literals with the shared constants already imported from testutils.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/blob/apis/service.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/blob/apis/service.test.ts b/tests/blob/apis/service.test.ts index e36844f50..8a829f528 100644 --- a/tests/blob/apis/service.test.ts +++ b/tests/blob/apis/service.test.ts @@ -839,9 +839,8 @@ describe("ServiceAPIs", () => { const host = server.config.host; const port = server.config.port; - const account = "devstoreaccount1"; - const accountKey = - "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; + const account = EMULATOR_ACCOUNT_NAME; + const accountKey = EMULATOR_ACCOUNT_KEY; function signAndSend( method: string,