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
12 changes: 12 additions & 0 deletions src/workerd/api/actor-state.c++
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,15 @@ class FacetOutgoingFactory final: public Fetcher::OutgoingFactory {
jsg::Ref<Fetcher> DurableObjectFacets::get(jsg::Lock& js,
kj::String name,
jsg::Function<jsg::Promise<StartupOptions>()> getStartupOptions) {
JSG_REQUIRE(name.size() <= MAX_FACET_NAME_LENGTH, TypeError, "Facet name is too long (max ",
MAX_FACET_NAME_LENGTH, " characters).");

auto& fm = getFacetManager();

JSG_REQUIRE(fm.getDepth() + 1 < MAX_FACET_TREE_DEPTH, Error,
"Facet nesting depth limit exceeded. The maximum depth including the root Durable Object is ",
MAX_FACET_TREE_DEPTH, ".");

auto& ioCtx = IoContext::current();

kj::Function<kj::Promise<Worker::Actor::FacetManager::StartInfo>()> getStartInfo =
Expand Down Expand Up @@ -1031,10 +1039,14 @@ jsg::Ref<Fetcher> DurableObjectFacets::get(jsg::Lock& js,
}

void DurableObjectFacets::abort(jsg::Lock& js, kj::String name, jsg::JsValue reason) {
JSG_REQUIRE(name.size() <= MAX_FACET_NAME_LENGTH, TypeError, "Facet name is too long (max ",
MAX_FACET_NAME_LENGTH, " characters).");
getFacetManager().abortFacet(name, js.exceptionToKj(reason));
}

void DurableObjectFacets::delete_(jsg::Lock& js, kj::String name) {
JSG_REQUIRE(name.size() <= MAX_FACET_NAME_LENGTH, TypeError, "Facet name is too long (max ",
MAX_FACET_NAME_LENGTH, " characters).");
getFacetManager().deleteFacet(name);
}

Expand Down
7 changes: 7 additions & 0 deletions src/workerd/api/actor-state.h
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,13 @@ class DurableObjectTransaction final: public jsg::Object, public DurableObjectSt

class DurableObjectFacets: public jsg::Object {
public:
// Maximum length of a facet name, in characters.
static constexpr size_t MAX_FACET_NAME_LENGTH = 256;

// Maximum depth of the facet tree, including the root Durable Object. Root is at depth 0, so
// the deepest allowed facet is at depth MAX_FACET_TREE_DEPTH - 1.
static constexpr uint MAX_FACET_TREE_DEPTH = 4;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: if these are only used directly within actor-state.c++, they likely don't need to be constants on the class and could be moved into an anonymous namespace in actor-state.c++


DurableObjectFacets(kj::Maybe<IoPtr<Worker::Actor::FacetManager>> facetManager)
: facetManager(kj::mv(facetManager)) {}

Expand Down
3 changes: 3 additions & 0 deletions src/workerd/io/worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,9 @@ class Worker::Actor final: public kj::Refcounted {
Worker::Actor::Id id;
};

// Returns the nesting depth of this facet. Root = 0, direct child of root = 1, etc.
virtual uint getDepth() const = 0;

// These methods are C++ equivalents of the JavaScript ctx.facets API.
virtual kj::Own<IoChannelFactory::ActorChannel> getFacet(
kj::StringPtr name, kj::Function<kj::Promise<StartInfo>()> getStartInfo) = 0;
Expand Down
140 changes: 140 additions & 0 deletions src/workerd/server/server-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -5020,6 +5020,146 @@ KJ_TEST("Server: Durable Object facets") {
}
}

KJ_TEST("Server: Durable Object facet limits") {
kj::StringPtr config = R"((
services = [
( name = "hello",
worker = (
compatibilityDate = "2025-04-01",
compatibilityFlags = ["experimental"],
modules = [
( name = "main.js",
esModule =
`import { DurableObject } from "cloudflare:workers";
`export default {
` async fetch(request, env, ctx) {
` let id = env.MY_ACTOR.idFromName("limits");
` let actor = env.MY_ACTOR.get(id);
` return await actor.fetch(request);
` }
`}
`export class MyActorClass extends DurableObject {
` async fetch(request) {
` let url = new URL(request.url);
` switch (url.pathname) {
` case "/name-too-long": {
` try {
` this.ctx.facets.get("x".repeat(257),
` () => ({class: this.env.RECURSIVE}));
` return new Response("no error");
` } catch (e) {
` return new Response(e.constructor.name + ": " + e.message);
` }
` }
` case "/name-256-ok": {
` this.ctx.facets.get("x".repeat(256),
` () => ({class: this.env.RECURSIVE}));
` return new Response("ok");
` }
` case "/abort-name-too-long": {
` try {
` this.ctx.facets.abort("x".repeat(257), new Error("test"));
` return new Response("no error");
` } catch (e) {
` return new Response(e.constructor.name + ": " + e.message);
` }
` }
` case "/delete-name-too-long": {
` try {
` this.ctx.facets.delete("x".repeat(257));
` return new Response("no error");
` } catch (e) {
` return new Response(e.constructor.name + ": " + e.message);
` }
` }
` case "/depth-ok": {
` // Create 3 levels of facets below root = 4 total (the max).
` let facet = this.ctx.facets.get("a",
` () => ({class: this.env.RECURSIVE}));
` return new Response(await facet.nestOk(2));
` }
` case "/depth-exceeded": {
` // Create 3 levels below root, then try one more.
` let facet = this.ctx.facets.get("b",
` () => ({class: this.env.RECURSIVE}));
` return new Response(await facet.nestDeeper(2));
` }
` }
` }
`}
`export class RecursiveFacet extends DurableObject {
` async nestOk(remaining) {
` if (remaining <= 0) return "ok";
` let facet = this.ctx.facets.get("child",
` () => ({class: this.env.RECURSIVE}));
` return await facet.nestOk(remaining - 1);
` }
` async nestDeeper(remaining) {
` if (remaining <= 0) {
` try {
` this.ctx.facets.get("too-deep",
` () => ({class: this.env.RECURSIVE}));
` return "no error, unexpected";
` } catch (e) {
` return e.constructor.name + ": " + e.message;
` }
` }
` let facet = this.ctx.facets.get("child",
` () => ({class: this.env.RECURSIVE}));
` return await facet.nestDeeper(remaining - 1);
` }
`}
)
],
bindings = [
(name = "MY_ACTOR", durableObjectNamespace = "MyActorClass"),
(name = "RECURSIVE",
durableObjectClass = (name = "hello", entrypoint = "RecursiveFacet"))
],
durableObjectNamespaces = [
( className = "MyActorClass",
uniqueKey = "mykey",
)
],
durableObjectStorage = (localDisk = "my-disk")
)
),
( name = "my-disk",
disk = (
path = "../../do-storage",
writable = true,
)
),
],
sockets = [
( name = "main",
address = "test-addr",
service = "hello"
)
]
))"_kj;

TestServer test(config);
test.root->openSubdir(kj::Path({"do-storage"_kj}), kj::WriteMode::CREATE);
test.server.allowExperimental();
test.start();
auto conn = test.connect("test-addr");

// Name length limit.
conn.httpGet200("/name-too-long", "TypeError: Facet name is too long (max 256 characters).");
conn.httpGet200("/name-256-ok", "ok");
conn.httpGet200(
"/abort-name-too-long", "TypeError: Facet name is too long (max 256 characters).");
conn.httpGet200(
"/delete-name-too-long", "TypeError: Facet name is too long (max 256 characters).");

// Depth limit.
conn.httpGet200("/depth-ok", "ok");
conn.httpGet200("/depth-exceeded",
"Error: Facet nesting depth limit exceeded. "
"The maximum depth including the root Durable Object is 4.");
}

KJ_TEST("Server: Pass service stubs in ctx.props.") {
TestServer test(R"((
services = [
Expand Down
7 changes: 7 additions & 0 deletions src/workerd/server/server.c++
Original file line number Diff line number Diff line change
Expand Up @@ -2489,6 +2489,13 @@ class Server::WorkerService final: public Service,
return entry.value->addRef();
}

uint getDepth() const override {
KJ_IF_SOME(p, parent) {
return 1 + p.getDepth();
}
return 0;
}

kj::Own<IoChannelFactory::ActorChannel> getFacet(
kj::StringPtr name, kj::Function<kj::Promise<StartInfo>()> getStartInfo) override {
auto facet = getFacetContainer(kj::str(name), kj::mv(getStartInfo));
Expand Down
Loading