Skip to content

Commit c4461ff

Browse files
NuroDevemily-shen
andauthored
fix(miniflare): Fix KV bulk/get 413 error for large namespaces (#13466)
Co-authored-by: emily-shen <69125074+emily-shen@users.noreply.github.com>
1 parent 409d23a commit c4461ff

4 files changed

Lines changed: 66 additions & 27 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@cloudflare/local-explorer-ui": patch
3+
---
4+
5+
Fix local explorer KV bulk / get for large payloads.
6+
7+
Fixes an issue where the local explorer UI would crash when fetching large KV payloads.
8+
9+
Additionally, the local KV bulk get API endpoint now enforces a total 25MB payload limit, in alignment with the remote Cloudflare API.

fixtures/worker-with-resources/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ const SEED_DATA: [string, string][] = [
236236
["number-integer", "42"],
237237
["number-float", "3.14159"],
238238
["number-negative", "-273.15"],
239+
["large-key-1", "x".repeat(10 * 1024 * 1024)],
239240
];
240241

241242
interface R2SeedItem {

packages/local-explorer-ui/src/__e2e__/kv/kv-namespace.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ describe("KV Namespace", () => {
2828
expect(rowText).toContain("Hello, World!");
2929
});
3030

31+
test("loads namespace values when aggregate payload exceeds bulk limit", async () => {
32+
await navigateToKV("KV");
33+
await waitForTableRows(5);
34+
await waitForText("large-key-1");
35+
});
36+
3137
test("shows column headers", async () => {
3238
await navigateToKV("KV");
3339
await waitForTableRows(1);

packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
import { useCallback, useEffect, useMemo, useState } from "react";
88
import {
99
workersKvNamespaceDeleteKeyValuePair,
10-
workersKvNamespaceGetMultipleKeyValuePairs,
1110
workersKvNamespaceListANamespace_SKeys,
1211
workersKvNamespaceReadKeyValuePair,
1312
workersKvNamespaceWriteKeyValuePairWithMetadata,
@@ -19,7 +18,7 @@ import { KVTable } from "../../components/KVTable";
1918
import { ResourceError } from "../../components/ResourceError";
2019
import { SearchForm } from "../../components/SearchForm";
2120
import { getSelectedWorker } from "../../components/WorkerSelector";
22-
import type { KVEntry } from "../../api";
21+
import type { KVEntry, WorkersKvKey } from "../../api";
2322

2423
export const Route = createFileRoute("/kv/$namespaceId")({
2524
component: NamespaceView,
@@ -31,27 +30,16 @@ export const Route = createFileRoute("/kv/$namespaceId")({
3130
});
3231
const keys = keysResponse.data?.result ?? [];
3332

34-
let values: Record<string, string | null> = {};
33+
let values = new Map<WorkersKvKey, string | null>();
3534
if (keys.length > 0) {
36-
const valuesResponse = await workersKvNamespaceGetMultipleKeyValuePairs({
37-
path: {
38-
namespace_id: params.namespaceId,
39-
},
40-
body: {
41-
keys: keys.map((k) => k.name),
42-
},
43-
});
44-
values = (valuesResponse.data?.result?.values ?? {}) as Record<
45-
string,
46-
string | null
47-
>;
35+
values = await readKVValues(params.namespaceId, keys);
4836
}
4937

5038
const cursor = keysResponse.data?.result_info?.cursor ?? null;
5139
const entries = keys.map(
5240
(key): KVEntry => ({
5341
key,
54-
value: values[key.name] ?? null,
42+
value: values.get(key) ?? null,
5543
})
5644
);
5745

@@ -87,6 +75,49 @@ const entryVisible = (entries: KVEntry[], key: string): boolean =>
8775

8876
const rootRoute = getRouteApi("__root__");
8977

78+
function isKeyNotFoundError(error: unknown): boolean {
79+
if (typeof error !== "object" || error === null || !("errors" in error)) {
80+
return false;
81+
}
82+
83+
const { errors } = error as {
84+
errors?: Array<{
85+
code?: number;
86+
}>;
87+
};
88+
89+
return errors?.some((entry) => entry.code === 10009) ?? false;
90+
}
91+
92+
async function readKVValues(
93+
namespaceId: string,
94+
keys: WorkersKvKey[]
95+
): Promise<Map<WorkersKvKey, string | null>> {
96+
const data = new Map<WorkersKvKey, string | null>();
97+
98+
await Promise.all(
99+
keys.map(async (key) => {
100+
try {
101+
const response = await workersKvNamespaceReadKeyValuePair({
102+
path: { namespace_id: namespaceId, key_name: key.name },
103+
parseAs: "text",
104+
});
105+
106+
data.set(key, response.data ?? null);
107+
} catch (error) {
108+
if (isKeyNotFoundError(error)) {
109+
data.set(key, null);
110+
return;
111+
}
112+
113+
throw error;
114+
}
115+
})
116+
);
117+
118+
return data;
119+
}
120+
90121
function NamespaceView() {
91122
const { namespaceId } = Route.useParams();
92123
const loaderData = Route.useLoaderData();
@@ -158,22 +189,14 @@ function NamespaceView() {
158189
});
159190
const keys = keysResponse.data?.result ?? [];
160191

161-
let values: Record<string, string | null> = {};
192+
let values = new Map<WorkersKvKey, string | null>();
162193
if (keys.length > 0) {
163-
const valuesResponse =
164-
await workersKvNamespaceGetMultipleKeyValuePairs({
165-
path: { namespace_id: namespaceId },
166-
body: { keys: keys.map((k) => k.name) },
167-
});
168-
values = (valuesResponse.data?.result?.values ?? {}) as Record<
169-
string,
170-
string | null
171-
>;
194+
values = await readKVValues(namespaceId, keys);
172195
}
173196

174197
const newEntries: KVEntry[] = keys.map((key) => ({
175198
key,
176-
value: values[key.name] ?? null,
199+
value: values.get(key) ?? null,
177200
}));
178201

179202
if (nextCursor) {

0 commit comments

Comments
 (0)