diff --git a/packages/miniflare/src/workers/kv/namespace.worker.ts b/packages/miniflare/src/workers/kv/namespace.worker.ts index 90efcf583..3bcf3db01 100644 --- a/packages/miniflare/src/workers/kv/namespace.worker.ts +++ b/packages/miniflare/src/workers/kv/namespace.worker.ts @@ -128,13 +128,20 @@ export class KVNamespaceObject extends MiniflareDurableObject { // through a transform stream to count it (trusting `workerd` to send // correct value here). let value = req.body; - assert(value !== null); // Safety of `!`: `parseInt(null)` is `NaN` // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const contentLength = parseInt(req.headers.get("Content-Length")!); - const valueLengthHint = Number.isNaN(contentLength) - ? undefined - : contentLength; + let valueLengthHint: number | undefined; + if (!Number.isNaN(contentLength)) valueLengthHint = contentLength; + else if (value === null) valueLengthHint = 0; + + // Empty values may be put with `null` bodies: + // https://github.com/cloudflare/miniflare/issues/703 + value ??= new ReadableStream({ + start(controller) { + controller.close(); + }, + }); const maxValueSize = this.beingTested ? KVLimits.MAX_VALUE_SIZE_TEST diff --git a/packages/miniflare/test/plugins/cache/index.spec.ts b/packages/miniflare/test/plugins/cache/index.spec.ts index 6759c1da2..0783f5c3b 100644 --- a/packages/miniflare/test/plugins/cache/index.spec.ts +++ b/packages/miniflare/test/plugins/cache/index.spec.ts @@ -99,6 +99,18 @@ test("match returns cached responses", async (t) => { t.is(res.status, 200); t.is(await res.text(), "buffered"); }); +test("match returns empty response", async (t) => { + const cache = t.context.caches.default; + const key = "http://localhost/cache-empty"; + const resToCache = new Response(null, { + headers: { "Cache-Control": "max-age=3600" }, + }); + await cache.put(key, resToCache); + const res = await cache.match(key); + assert(res !== undefined); + t.is(res.status, 200); + t.is(await res.text(), ""); +}); test("match returns nothing on cache miss", async (t) => { const cache = t.context.caches.default; const key = "http://localhost/cache-miss"; diff --git a/packages/miniflare/test/plugins/kv/index.spec.ts b/packages/miniflare/test/plugins/kv/index.spec.ts index 2be608c27..e5576fefc 100644 --- a/packages/miniflare/test/plugins/kv/index.spec.ts +++ b/packages/miniflare/test/plugins/kv/index.spec.ts @@ -152,6 +152,13 @@ test("put: puts value", async (t) => { const results = await kv.list({ prefix: ns }); t.is(results.keys[0]?.expiration, TIME_FUTURE); }); +test("put: puts empty value", async (t) => { + // https://github.com/cloudflare/miniflare/issues/703 + const { kv } = t.context; + await kv.put("key", ""); + const value = await kv.get("key"); + t.is(value, ""); +}); test("put: overrides existing keys", async (t) => { const { kv } = t.context; await kv.put("key", "value1"); diff --git a/packages/miniflare/test/plugins/r2/index.spec.ts b/packages/miniflare/test/plugins/r2/index.spec.ts index c999ccdac..b40a86f2f 100644 --- a/packages/miniflare/test/plugins/r2/index.spec.ts +++ b/packages/miniflare/test/plugins/r2/index.spec.ts @@ -376,6 +376,14 @@ test("put: returns metadata for created object", async (t) => { t.is(object.range, undefined); isWithin(t, WITHIN_EPSILON, object.uploaded.getTime(), start); }); +test("put: puts empty value", async (t) => { + const { r2 } = t.context; + const object = await r2.put("key", ""); + assert(object !== null); + t.is(object.size, 0); + const objectBody = await r2.get("key"); + t.is(await objectBody?.text(), ""); +}); test("put: overrides existing keys", async (t) => { const { r2, ns, object } = t.context; await r2.put("key", "value1");