Repository navigation
π Bug Report β Runtime APIs: structuredClone() and Durable Object storage serialize streams as {} with typescript_implemented_streamsΒ #8046
Description
Description
With typescript_implemented_streams, Web Streams objects are serialized as empty plain objects instead of being rejected. structuredClone(new ReadableStream()) returns {}, and so does postMessage(). Durable Object storage.put("key", stream) succeeds and stores {}.
The C++ implementation throws DataCloneError for all of these, and the spec agrees: streams are transferable but not serializable. Every stream-related class I tried does this:
ReadableStream,WritableStream,TransformStream- readers, writers and controllers
ByteLengthQueuingStrategy,CountQueuingStrategyTextDecoderStream,CompressionStreamIdentityTransformStream,FixedLengthStream- a
Responsebody
Nested values serialize too: structuredClone({ a: [stream] }) gives { a: [{}] }. The serializer appears to treat the TypeScript classes as ordinary objects with no own enumerable properties.
The silent {} is the problem. A Durable Object that persists a value containing a stream now loses the data without any error. JS RPC is not affected: passing a TransformStream, reader or writer over RPC still throws DataCloneError, and streams are still transferred.
Reproduction
worker.js:
import { DurableObject } from "cloudflare:workers";
export class Store extends DurableObject {
async save() {
await this.ctx.storage.put("body", new ReadableStream());
return JSON.stringify(await this.ctx.storage.get("body"));
}
}
export default {
async test(ctrl, env) {
let failed = false;
for (const value of [new ReadableStream(), new WritableStream(), new TransformStream(), new CompressionStream("gzip")]) {
const name = value.constructor.name;
try {
const clone = structuredClone(value);
console.log(`structuredClone(${name}) returned ${JSON.stringify(clone)}`);
failed = true;
} catch (error) {
console.log(`structuredClone(${name}) threw ${error.name}`);
}
}
try {
const stored = await env.STORE.get(env.STORE.idFromName("x")).save();
console.log(`storage.put(ReadableStream) stored ${stored}`);
failed = true;
} catch (error) {
console.log(`storage.put(ReadableStream) threw ${error.name}`);
}
if (failed) throw new Error("a stream was serialized");
},
};config.capnp:
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [(name = "main", worker = .main)],
# typescript_implemented_streams only starts with this autogate.
autogates = ["workerd-autogate-per-isolate-javascript-bootstrap"],
);
const main :Workerd.Worker = (
modules = [(name = "worker", esModule = embed "worker.js")],
compatibilityDate = "2026-10-27",
compatibilityFlags = ["typescript_implemented_streams"],
durableObjectNamespaces = [(className = "Store", uniqueKey = "store")],
durableObjectStorage = (inMemory = void),
bindings = [(name = "STORE", durableObjectNamespace = "Store")],
);workerd test --experimental config.capnpWith the flag (workerd 2026-10-27, @cloudflare/workerd-darwin-arm64@1.20261027.1):
structuredClone(ReadableStream) returned {}
structuredClone(WritableStream) returned {}
structuredClone(TransformStream) returned {}
structuredClone(CompressionStream) returned {}
storage.put(ReadableStream) stored {}
[ FAIL ] main
With compatibilityFlags = [] (C++ streams):
structuredClone(ReadableStream) threw DataCloneError
structuredClone(WritableStream) threw DataCloneError
structuredClone(TransformStream) threw DataCloneError
structuredClone(CompressionStream) threw DataCloneError
storage.put(ReadableStream) threw DataCloneError
[ PASS ] main
Expected behavior
Serializing a stream, reader, writer, controller, queuing strategy or transform stream throws DataCloneError, as it does with the C++ implementation. That covers structuredClone(), postMessage() and Durable Object storage.
Activity
jasnell commentedon Oct 27, 2026
This is another known item that we need to decide more generally on. C++ impls of APIs will throw exceptions if they are structuredClone'd... TypeScript impls do not and instead come through as empty ordinary objects. We can have them throw but I'm not sure it's worthwhile.
@guybedford any opinions?