Skip to content
Open
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
24 changes: 24 additions & 0 deletions src/workerd/api/blob.c++
Original file line number Diff line number Diff line change
Expand Up @@ -347,4 +347,28 @@ jsg::Ref<File> File::constructor(
return js.alloc<File>(kj::mv(name), kj::mv(type), lastModified);
}

void Blob::serialize(jsg::Lock& js, jsg::Serializer& serializer) {
serializer.writeLengthDelimited(type);
serializer.writeLengthDelimited(data);
}

jsg::Ref<Blob> Blob::deserialize(
jsg::Lock& js, rpc::SerializationTag tag, jsg::Deserializer& deserializer) {
auto type = deserializer.readLengthDelimitedString();

// Guard against a bogus length triggering a huge allocation.
uint64_t size = deserializer.readRawUint64();
auto limit = Worker::Isolate::from(js).getLimitEnforcer().getBlobSizeLimit();
JSG_REQUIRE(size <= limit, DOMDataCloneError, "Deserialization failed: Blob size exceeds limit (",
size, ")");

if (size == 0) {
return js.alloc<Blob>(kj::mv(type));
}

auto bytes = deserializer.readRawBytes(size);
auto u8 = jsg::JsUint8Array::create(js, bytes);
return js.alloc<Blob>(js, jsg::JsBufferSource(u8), kj::mv(type));
}

} // namespace workerd::api
8 changes: 8 additions & 0 deletions src/workerd/api/blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
#pragma once

#include <workerd/io/compatibility-date.capnp.h>
#include <workerd/io/worker-interface.capnp.h>
#include <workerd/jsg/jsg.h>
#include <workerd/jsg/ser.h>

namespace workerd::api {

Expand Down Expand Up @@ -78,6 +80,12 @@ class Blob: public jsg::Object {
});
}

// Serialized as MIME type + raw bytes.
void serialize(jsg::Lock& js, jsg::Serializer& serializer);
static jsg::Ref<Blob> deserialize(
jsg::Lock& js, rpc::SerializationTag tag, jsg::Deserializer& deserializer);
JSG_SERIALIZABLE(rpc::SerializationTag::BLOB);

void visitForMemoryInfo(jsg::MemoryTracker& tracker) const {
KJ_SWITCH_ONEOF(ownData) {
KJ_CASE_ONEOF(_, Empty) {}
Expand Down
21 changes: 20 additions & 1 deletion src/workerd/api/tests/global-scope-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export const unhandledRejectionHandler4 = {
};

export const structuredClone = {
test() {
async test() {
{
strictEqual(globalThis.structuredClone('hello'), 'hello');
}
Expand Down Expand Up @@ -307,6 +307,25 @@ export const structuredClone = {
strictEqual(cloned.get('bar'), 'abc');
}

// Blob is a serializable platform object.
{
let orig = new Blob(['abc', 'def'], { type: 'text/plain' });
let cloned = globalThis.structuredClone(orig);
ok(cloned instanceof Blob);
notStrictEqual(cloned, orig);
strictEqual(cloned.type, 'text/plain');
strictEqual(cloned.size, orig.size);
strictEqual(await cloned.text(), 'abcdef');
}

// An empty Blob round-trips too.
{
let cloned = globalThis.structuredClone(new Blob([]));
ok(cloned instanceof Blob);
strictEqual(cloned.size, 0);
strictEqual(cloned.type, '');
}

// Verify that trying to serialize a non-serializable API type throws.
throws(() => globalThis.structuredClone(new TextEncoder()), {
name: 'DataCloneError',
Expand Down
20 changes: 20 additions & 0 deletions src/workerd/api/tests/js-rpc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,26 @@ export let serializeHttpTypes = {
},
};

export let serializeBlob = {
async test(controller, env, ctx) {
// Blob round-trips over RPC, both directly and nested in a plain object.
let blob = await env.MyService.roundTrip(
new Blob(['hello ', 'world'], { type: 'text/plain' })
);
assert.ok(blob instanceof Blob);
assert.strictEqual(blob.type, 'text/plain');
assert.strictEqual(await blob.text(), 'hello world');

let doc = await env.MyService.roundTrip({
name: 'doc.txt',
blob: new Blob(['contents']),
});
assert.strictEqual(doc.name, 'doc.txt');
assert.ok(doc.blob instanceof Blob);
assert.strictEqual(await doc.blob.text(), 'contents');
},
};

// Test that exceptions thrown from async native functions have a proper stack trace. (This is
// not specific to RPC but RPC is a convenient place to test it since we can easily define the
// callee to throw an exception.)
Expand Down
19 changes: 10 additions & 9 deletions src/workerd/api/tests/messageport-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,16 @@ export const simple5 = {
// Refs: https://github.com/web-platform-tests/wpt/blob/master/webmessaging/Channel_postMessage_DataCloneErr.any.js

export const postMessageBlob = {
test() {
// Per the spec, Blob is a serializable object, but we don't implement it as such currently.
// So attempting to post a blob should throw an error.
const { port1 } = new MessageChannel();
throws(() => port1.postMessage(new Blob([''])), {
code: 25, // DATA_CLONE_ERR,
name: 'DataCloneError',
message: /Could not serialize/,
});
async test() {
// Per the spec, Blob is a serializable object, so it round-trips through postMessage.
const { port1, port2 } = new MessageChannel();
const { promise, resolve } = Promise.withResolvers();
port2.onmessage = (event) => resolve(event.data);
port1.postMessage(new Blob(['hello'], { type: 'text/plain' }));
const received = await promise;
ok(received instanceof Blob);
strictEqual(received.type, 'text/plain');
strictEqual(await received.text(), 'hello');
},
};

Expand Down
3 changes: 3 additions & 0 deletions src/workerd/io/worker-interface.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,9 @@ enum SerializationTag {
#
# Similar to serviceStub, this refers to the entrypoint of a Worker that can be instantiated
# anywhere and any time, and thus can be persisted and used in `env` and `ctx.props`, etc.

blob @13;
# A Blob, serialized as its MIME type followed by its raw bytes.
}

enum StreamEncoding {
Expand Down
8 changes: 6 additions & 2 deletions src/workerd/server/tests/python/python-rpc/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,16 @@ async def test(ctrl, env, ctx):
py_response2 = await env.PythonRpc.handle_response(js.Response.new("a JS response"))
assert await py_response2.text() == "a JS response"

# - Verify that a Blob can be round-tripped.
blob = await env.PythonRpc.identity(Blob("print(42)", content_type="text/python"))
assert isinstance(blob, Blob)
assert blob.content_type == "text/python"
assert await blob.text() == "print(42)"

# Verify that sending unsupported types fails.
data_clone_regex = "^DataCloneError"
with assertRaisesRegex(JsException, data_clone_regex):
await env.PythonRpc.one_arg(CustomType(42))
with assertRaisesRegex(JsException, data_clone_regex):
await env.PythonRpc.one_arg(Blob("print(42)", content_type="text/python"))
with assertRaisesRegex(JsException, data_clone_regex):
await env.PythonRpc.identity(complex(1.23))
with assertRaises(TypeError):
Expand Down
Loading