Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 112fa5b

Browse files
committed
Fix KVNamespace#put() with empty value
Empty `put()`s may send HTTP requests with `null` bodies, violating an assumption we had. This change handles this case, and adds some additional tests for empty values. Closes #703
1 parent 7d0ed16 commit 112fa5b

File tree

4 files changed

+38
-4
lines changed

4 files changed

+38
-4
lines changed

packages/miniflare/src/workers/kv/namespace.worker.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,20 @@ export class KVNamespaceObject extends MiniflareDurableObject {
128128
// through a transform stream to count it (trusting `workerd` to send
129129
// correct value here).
130130
let value = req.body;
131-
assert(value !== null);
132131
// Safety of `!`: `parseInt(null)` is `NaN`
133132
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
134133
const contentLength = parseInt(req.headers.get("Content-Length")!);
135-
const valueLengthHint = Number.isNaN(contentLength)
136-
? undefined
137-
: contentLength;
134+
let valueLengthHint: number | undefined;
135+
if (!Number.isNaN(contentLength)) valueLengthHint = contentLength;
136+
else if (value === null) valueLengthHint = 0;
137+
138+
// Empty values may be put with `null` bodies:
139+
// https://github.com/cloudflare/miniflare/issues/703
140+
value ??= new ReadableStream<Uint8Array>({
141+
start(controller) {
142+
controller.close();
143+
},
144+
});
138145

139146
const maxValueSize = this.beingTested
140147
? KVLimits.MAX_VALUE_SIZE_TEST

packages/miniflare/test/plugins/cache/index.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ test("match returns cached responses", async (t) => {
9999
t.is(res.status, 200);
100100
t.is(await res.text(), "buffered");
101101
});
102+
test("match returns empty response", async (t) => {
103+
const cache = t.context.caches.default;
104+
const key = "http://localhost/cache-empty";
105+
const resToCache = new Response(null, {
106+
headers: { "Cache-Control": "max-age=3600" },
107+
});
108+
await cache.put(key, resToCache);
109+
const res = await cache.match(key);
110+
assert(res !== undefined);
111+
t.is(res.status, 200);
112+
t.is(await res.text(), "");
113+
});
102114
test("match returns nothing on cache miss", async (t) => {
103115
const cache = t.context.caches.default;
104116
const key = "http://localhost/cache-miss";

packages/miniflare/test/plugins/kv/index.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ test("put: puts value", async (t) => {
152152
const results = await kv.list({ prefix: ns });
153153
t.is(results.keys[0]?.expiration, TIME_FUTURE);
154154
});
155+
test("put: puts empty value", async (t) => {
156+
// https://github.com/cloudflare/miniflare/issues/703
157+
const { kv } = t.context;
158+
await kv.put("key", "");
159+
const value = await kv.get("key");
160+
t.is(value, "");
161+
});
155162
test("put: overrides existing keys", async (t) => {
156163
const { kv } = t.context;
157164
await kv.put("key", "value1");

packages/miniflare/test/plugins/r2/index.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,14 @@ test("put: returns metadata for created object", async (t) => {
376376
t.is(object.range, undefined);
377377
isWithin(t, WITHIN_EPSILON, object.uploaded.getTime(), start);
378378
});
379+
test("put: puts empty value", async (t) => {
380+
const { r2 } = t.context;
381+
const object = await r2.put("key", "");
382+
assert(object !== null);
383+
t.is(object.size, 0);
384+
const objectBody = await r2.get("key");
385+
t.is(await objectBody?.text(), "");
386+
});
379387
test("put: overrides existing keys", async (t) => {
380388
const { r2, ns, object } = t.context;
381389
await r2.put("key", "value1");

0 commit comments

Comments
 (0)