Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
126 changes: 126 additions & 0 deletions tests/blob/apis/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,132 @@ 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 = EMULATOR_ACCOUNT_NAME;
const accountKey = EMULATOR_ACCOUNT_KEY;

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