Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/common-worms-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"miniflare": patch
---

KV: improve error messages for bulk gets
1 change: 1 addition & 0 deletions packages/miniflare/src/workers/kv/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const KVLimits = {
MAX_VALUE_SIZE: 25 * 1024 * 1024 /* 25MiB */,
MAX_VALUE_SIZE_TEST: 1024 /* 1KiB */,
MAX_METADATA_SIZE: 1024 /* 1KiB */,
MAX_BULK_SIZE: 25 * 1024 * 1024 /* 25MiB */,
} as const;

export const KVParams = {
Expand Down
33 changes: 24 additions & 9 deletions packages/miniflare/src/workers/kv/namespace.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ async function processKeyValue(
}

let val = null;
const size = decodedValue.length;
try {
val = !obj?.value
? null
Expand All @@ -99,16 +100,19 @@ async function processKeyValue(
} catch (err: any) {
throw new HttpError(
400,
"At least one of the requested keys corresponds to a non-JSON value"
`At least one of the requested keys corresponds to a non-${type} value`
);
}
if (val && withMetadata) {
return {
value: val,
metadata: obj?.metadata ?? null,
};
return [
{
value: val,
metadata: obj?.metadata ?? null,
},
size,
];
}
return val;
return [val, size];
}

export class KVNamespaceObject extends MiniflareDurableObject {
Expand All @@ -132,24 +136,35 @@ export class KVNamespaceObject extends MiniflareDurableObject {
const keys: string[] = parsedBody.keys;
const type = parsedBody?.type;
if (type && type !== "text" && type !== "json") {
return new Response(`Type ${type} is invalid`, { status: 400 });
return new Response("Bad Request", { status: 400 });
}
const obj: { [key: string]: any } = {};
if (keys.length > MAX_BULK_GET_KEYS) {
return new Response(`Accepting a max of 100 keys, got ${keys.length}`, {
return new Response("Bad Request", {
status: 400,
});
}
let totalBytes = 0;
for (const key of keys) {
validateGetOptions(key, { cacheTtl: parsedBody?.cacheTtl });
const entry = await this.storage.get(key);
const value = await processKeyValue(
const [value, size] = await processKeyValue(
entry,
parsedBody?.type,
parsedBody?.withMetadata
);
totalBytes += size;
obj[key] = value;
}
const maxValueSize = this.beingTested
? KVLimits.MAX_VALUE_SIZE_TEST
: KVLimits.MAX_BULK_SIZE;
if (totalBytes > maxValueSize) {
throw new HttpError(
413,
`Total size of request exceeds the limit of ${maxValueSize / 1024 / 1024}MB`
);
}

return new Response(JSON.stringify(obj));
}
Expand Down
17 changes: 16 additions & 1 deletion packages/miniflare/test/plugins/kv/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ test("bulk get: request json type", async (t) => {
} catch (error: any) {
t.is(
error.message,
"KV GET_BULK failed: 400 At least one of the requested keys corresponds to a non-JSON value"
"KV GET_BULK failed: 400 At least one of the requested keys corresponds to a non-json value"
);
}
});
Expand Down Expand Up @@ -224,6 +224,21 @@ test("bulk get: get with metadata for 404", async (t) => {
t.deepEqual(result, expectedResult);
});

test("bulk get: get over size limit", async (t) => {
const { kv } = t.context;
const bigValue = new Array(1024).fill("x").join("");
await kv.put("key1", bigValue);
await kv.put("key2", bigValue);
try {
await kv.getWithMetadata(["key1", "key2"]);
} catch (error: any) {
t.deepEqual(
error.message,
"KV GET_BULK failed: 413 Total size of request exceeds the limit of 0.0009765625MB" // 1024 Bytes for testing
);
}
});

test("get: returns null for non-existent keys", async (t) => {
const { kv } = t.context;
t.is(await kv.get("key"), null);
Expand Down
Loading