Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/blob/handlers/ContainerHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || ""}`
Expand Down
2 changes: 1 addition & 1 deletion src/blob/handlers/ServiceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || ""}`
Expand Down
127 changes: 127 additions & 0 deletions tests/blob/apis/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,133 @@

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<string, string> = {
"x-ms-date": now,
"x-ms-version": "2021-10-04"
};
Comment on lines +851 to +854

// 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<string, string[]> = {};
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();
});
}
Comment on lines +845 to +920

// 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("<Blobs"),
"Response should contain a <Blobs> 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("<Blobs"),
"Response should contain a <Blobs> element"
);

Comment on lines +922 to +952
await containerClient.delete();
});
});

describe("ServiceAPIs - secondary location endpoint", () => {
Expand Down
Loading