R2
env.<BINDING> of an r2_buckets entry: get, put, head, delete, list, conditional
requests and multipart uploads. Objects are not kept in a cell: like celld, CellB writes them
directly to storage.
{
"name": "r2",
"main": "index.js",
"compatibility_date": "2026-01-01",
"r2_buckets": [
{ "binding": "FILES", "bucket_name": "example-files" }
]
}
export default {
async fetch(request, env) {
const key = new URL(request.url).pathname.slice(1);
if (!key) return new Response("Use /KEY.", { status: 400 });
if (request.method === "PUT") {
await env.FILES.put(key, request.body, {
httpMetadata: {
contentType: request.headers.get("content-type") ??
"application/octet-stream",
},
});
return new Response(null, { status: 204 });
}
if (request.method === "DELETE") {
await env.FILES.delete(key);
return new Response(null, { status: 204 });
}
if (request.method === "GET") {
const object = await env.FILES.get(key);
if (object === null) return new Response("Not found.", { status: 404 });
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
return new Response(object.body, { headers });
}
return new Response("Method not allowed.", { status: 405 });
},
};
This is celld’s r2 example (conformance/celld-examples/r2), copied unchanged
and run by the conformance suite on Node and in Chromium.
Where objects live depends on the storage
| Storage | Object | Metadata | An open multipart upload |
|---|---|---|---|
| A fleet bucket (shared with celld) | r2/<bucket>/<key>, celld’s layout | the user-metadata entry celld-r2 | its parts under r2-uploads/<bucket>/<uploadId>/ |
| The project’s FilesApi (no fleet bucket) | objects/<encoded key> | meta/<encoded key>.json | multipart/<uploadId>/ |
An object written by a single request has an MD5 ETag; a multipart object has none, as on celld.
On a fleet bucket, objects written by put and by multipart upload cross between celld and CellB
with their metadata.
How it is checked
celld’s r2 example and CellB’s r2-api, r2-advanced and r2-details
projects, on Node and in Chromium; e2e/p11f-celld-one-bucket.test.ts moves objects both ways.
What breaks
- A multipart upload opened on one runtime cannot be finished on the other. celld keeps an open
upload in its node’s memory and answers
R2 multipart upload <id> is not open on this node: …; CellB answerscompleteMultipartUpload: The specified multipart upload does not exist. (10024)for an upload celld opened. Complete an upload where it was opened; the finished object reads on both. - In
celld dev, R2 ETags are a store generation number; CellB’s are the store’s ETag. - Errors carry celld’s messages:
R2 <op> (binding <NAME>): ….