workerd icon indicating copy to clipboard operation
workerd copied to clipboard

bug: `DurableObjectId::id.name` doesn't work

Open threepointone opened this issue 1 year ago • 11 comments

(maybe this is a feature request?)

When creating a durable object with .idFromName(name), I would expect this.ctx.id.name inside a durable object. to reflect the same. The types imply that it should it work, was it just missed?

We use this name extensively, but currently have to hack it in by passing it in with an explicit method, but it's super cumbersome to do so without a bunch of hacks and making sure it's passed before any subsequent requests/methods are called. It would simplify a lot if workerd/workers/wrangler could provide it by default. Please and thank you!

threepointone avatar Jun 08 '24 09:06 threepointone

The "hack"/workaround we have also doesn't work for alarms, so fixing this would fix that bug as well.

threepointone avatar Jun 09 '24 11:06 threepointone

This bothers me too, but I can't figure out how to make it work. The thing is, you are allowed to do:

let id = ns.idFromName("foo");
let id2 = ns.idFromString(id.toString());
ns.get(id2);

In this case, we've lost the name, because id.toString() only encodes the hex ID, not the name. How do we support this?

Moreover, for alarms, where does the name get stored? Does everything in the system which stores a DO ID need to store the name too, or do we implicitly store the name in the DO's own metadata?

Storing it in the metadata seems highly preferable (I guess it would get initialized the first time a name-bearing ID is used?), but then this gets weird when you consider that DOs are implicitly deleted if they shut down while their storage is empty. So the name has to be forgotten at that point. But only if the storage is empty? Is that weird?

kentonv avatar Jun 09 '24 14:06 kentonv

I was meaning to ask, why is that pattern even around? I kinda remember it being in the docs, but can't find it there anymore (and rewrote some of my non-released code to not have that intermediate step anymore).

I think we can simply message the conditions under which that name will exist.

  • It won't exist if using newUniqueId()
  • it won't exist if using the above pattern (or rather, it'll return the hex id instead)
  • it's only ns.get(ns.idForName(...)). This might exclude any existing DOs that rewrite to this pattern, but that's a hit we should take

I guess it would get initialized the first time a name-bearing ID is used?)

Yes

his gets weird when you consider that DOs are implicitly deleted if they shut down while their storage is empty. So the name has to be forgotten at that point. But only if the storage is empty? Is that weird?

Oh hmm. Does that mean it won't be available in alarms? In which case, can we store the name everywhere the ID is stored? Or does setting an alarm mean that storage isn't empty? In which case, it's fine, since it'll get the name again when it comes back to life.

threepointone avatar Jun 09 '24 14:06 threepointone

The next alarm time counts as a stored value, so when an alarm is present then storage is not empty.

There are other built-in features planned which are likely to store hex IDs and where we wouldn't really want those systems to have to think about storing names alongside them.

But perhaps we can be reasonably confident that future features similar to alarms that store a hex ID only are never going to be waking up an empty object? And therefore the only way an object might not know its name is if application code wakes up an empty object by ID? Maybe that's OK?

Another weird issue: Technically someone could be passing in a multi-megabyte string as a name, which works fine if it's just getting hashed down but not so fine if we have to transmit that name over the network implicitly. Maybe there's some threshold at which we refuse to automatically store the name for them?

kentonv avatar Jun 09 '24 15:06 kentonv

And therefore the only way an object might not know its name is if application code wakes up an empty object by ID? Maybe that's OK?

Yeah, I think that's ok too

Maybe there's some threshold at which we refuse to automatically store the name for them?

Also ok! Maybe we disallow giant names at all, unless there's a usecase I don't know.

threepointone avatar Jun 10 '24 06:06 threepointone

for when @joshthoward is back, maybe a good first issue for July onboarding folks

vy-ton avatar Jun 14 '24 13:06 vy-ton

We have a common pattern in Prisma where we name DOs based on some value that we also need inside the DO. One such example is our tenant ID which is a value created by newUniqueId() of one DO initially and then used to as the name of other DO classes that are spawned from that tenant (often from additional workflows). Those spawned DOs often need their tenant ID value to do their work, so we pass it in on an initialization call and put it into storage. This is a bit awkward as we need to forward the tenant ID with any calls into the spawned DO just in case it is the first time it is being initialized for that tenant.

RpcTarget is useful here as we can improve the interaction with these DOs by having a single RPC method for initializing with a tenant ID (or other context) which returns an RpcTarget implementation containing the actual operations. This ensures that we provide the appropriate initialization context while not having to awkwardly include it as part of every method or HTTP request on the DO. I suppose it's the same end result, though it feels much nicer to work with.

rtbenfield avatar Jun 19 '24 14:06 rtbenfield

Myself and @boristane just ran into this issue.

It's really confusing and breaks the abstraction a DO can provide when doing multi-tenancy using a durable object.

Ankcorn avatar Aug 06 '24 09:08 Ankcorn

It would be excellent to be able to get id in DO instances, we currently workaround this too for Workflows

sidharthachatterjee avatar Aug 15 '24 13:08 sidharthachatterjee

ns.idFromName("foo")

this does a lookup to get the hex id from "foo" since any worker calling it needs to get the same hex id. Can a DO do a reverse lookup on instantiation (or only in the getter for ctx.id.name since many do not use it) to see if there is any name associated with it's hex id?

jacwright avatar Oct 15 '24 23:10 jacwright

this does a lookup to get the hex id from "foo" since any worker calling it needs to get the same hex id.

It actually doesn't do a lookup. It uses a cryptographic hash function to generate the ID based on the name.

Can a DO do a reverse lookup

No, because hash functions are one-way.

kentonv avatar Oct 16 '24 17:10 kentonv

I'm encountering a similar issue to what @rtbenfield described earlier. In our multi-tenant event storage system, we use the tenant ID as the name to facilitate communication between multiple Durable Objects across different namespaces. Currently, we rely on a top-level "management" Durable Object to store and retrieve the mapping of IDs for other Durable Objects. This approach requires additional roundtrips, which adds complexity and latency to our system.

Questions:

  1. Are there any updates or planned features that would allow us to access the Durable Object's name directly from within the object?
  2. Is using a management Durable Object to handle ID mappings a common pattern?
  3. What alternative workarounds have you found effective in similar scenarios?

fernando-barroso avatar Dec 11 '24 16:12 fernando-barroso

  • I can't make any promises for the team, but iiuc getting the name inside a durable object instance (with some constraints) is on the roadmap.
  • Using a management Durable Object (or even, just a database table) to handle mappings is indeed common. For example, a database that holds chatrooms that have already been created so they can be enumerated and listed before a user enters one of them.
  • An alternative is to read the name from the url of the first request (or when using plain rpc, expose an "initialise" function that gets executed before anything else) and store the name of the DO in .storage.

threepointone avatar Dec 21 '24 13:12 threepointone

We are also interested on this. The company I work on is facing observability issues since we cannot "log" the name from within the durable object without hacks.

IMHO it should be ok to save it as part of the metadata of the DO, even when it gets lost if the storage is empty. Since the name -> id mapping is done through a cryptographic hash function, it will always result in the same id.

romeupalos avatar Feb 14 '25 08:02 romeupalos

Like many others, we're having to jump through hoops to work around this issue - any guidance re where on the roadmap this sits? Higher priority, lower priority?

marbemac avatar Mar 09 '25 22:03 marbemac

Just adding my +1 here! Been able to work around this with the tips @threepointone gave :)

OCA99 avatar May 10 '25 17:05 OCA99

Is this fixed?! this.ctx.id.name seems to be logging out the name now - at least w latest local wrangler, have not tried in prod cloudflare runtime

marbemac avatar Jun 05 '25 21:06 marbemac

@marbemac sorry to say, this is just unintended behaviour in local dev, and will probably be removed soon.

threepointone avatar Jun 08 '25 13:06 threepointone

Ah ok bummer - maybe some day 😅

marbemac avatar Jun 08 '25 19:06 marbemac

Would be great to have it working like @marbemac said. 😄 I was under the false impression that it was supposed to work since my local dev worked fine, but couldn't figure out why name was undefined in prod until I got here. I guess it is better to remove from local dev until then.

bgever avatar Jun 09 '25 01:06 bgever

Yes sorry, I inadvertently caused this when fixing a different bug: For a long time, in local development, ctx.id was simply a string, not a DurableObjectId. I fixed that, but my fix inadvertently delivers an ID that (sometimes) knows its own name, which is not the case in production.

We all agree that it would be nice to deliver the actual name in production but, again, the system actually allows sending a message to a DO by ID alone without ever giving the name, so it's impossible to guarantee, and it's tricky to figure out a reasonable subset of cases where we can guarantee the name is available such that apps can reasonably rely on it...

kentonv avatar Jun 09 '25 14:06 kentonv

Thanks for that fix, that's good to have even with the side effect in local dev.

In the meantime it may help others to add some guidance regarding this behavior in the docs here, https://developers.cloudflare.com/durable-objects/api/id/#name, and possibly put a reference to that on the TSDoc of the name property when accessed from the ctx in the constructor. That would have helped me realize what is going on.

Copilot advised me on the registry approach mentioned earlier in the thread, but I couldn't find official guidance regarding this in the Cloudflare docs. That might also be nice to add to the docs.

bgever avatar Jun 09 '25 14:06 bgever

In my opinion it'd be very useful to store the name somewhere and make it accessible in more places. The ID is meaningless as it has no references anywhere since I hardcode names in my code (I think most people do), and name is not inferrable from id (afaik).

There's this endpoint I used in https://github.com/janwilmake/do-instance-viewer to get all DO instances, but it has just the ids and whether or not it has data. Also it's not available without cloudflare account ID and api key.

I tried solving this by making my own name registry in the DO, in https://github.com/janwilmake/nameable-object, but also that isn't possible because the DO can't know its own name, so there's no easy way to do this cleanly (without doing a separate fetch passing down these details).

Any suggestions on how solve this (to create a DO instances dashboard) are welcomed. Or more clarity on what's on the roadmap about this.

I can imagine you could:

  • Attach name in your internal DO instance registry and augment the response of https://oapis.org/openapi/cloudflare/durable-objects-namespace-list-objects
  • Create a new API DurableObjectNamespace.list() => Promise<{hasStoredData:boolean,id:string,name?:string}> (make that endpoint easy to access from workers)

janwilmake avatar Jun 24 '25 14:06 janwilmake

As a workaround, you can do exhaustive search over all possible names within the DO constructor until you find the id that matches the current DO id. This obviously only works if your Durable Object has a fixed set of possible names known within the DO constructor (e.g., coming from an embedded configuration), but was much more ergonomic than parameter passing for my application, at least.

Example:

import { DurableObject } from 'cloudflare:workers';

const possible_names = ['foo', 'bar'];

export class MyDurableObject extends DurableObject<Env> {
    name: string | undefined;
    constructor(ctx: DurableObjectState, env: Env) {
        super(ctx, env);
        for (const name of possible_names) {
          const cand_id = env.MY_DURABLE_OBJECT.idFromName(name);
          console.log(name, ctx.id, cand_id);
          if (ctx.id.equals(cand_id)) {
            this.name = name; 
          }
        }
    }

    async getMsg(): Promise<string> {
        return this.name ?? '<no id>';
    }
}

export default {
    async fetch(request, env, ctx): Promise<Response> {
        const id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName('foo');
        const stub = env.MY_DURABLE_OBJECT.get(id);
        return new Response(`${id.name}:${await stub.getMsg()}`);
    },
} satisfies ExportedHandler<Env>;

This will return foo:foo. https://durable-object-name-worker-example.luke-valenta.workers.dev/

lukevalenta avatar Jul 10 '25 12:07 lukevalenta

Just ran into this, and it breaks the ability to easily support multi-tenancy in DOs.

My particular case is as follows:

  1. GET request to the worker with an x-api-key header.
  2. Look up API key in Worker KV
  3. Validate, and get back tenant details (tenantId, envId, etc)
  4. Construct a name:
    • tenant-${tenantId}:env-${envId}
  5. Convert name to DurableObjectId:
    • const doId = env.DURABLE_OBJECT.idFromName(name);
  6. Invoke the DO
    • const stub = env.DURABLE_OBJECT.get(doId);

Expectation:

Within the DO, since the doId was created with idFromName, this.ctx.id.name would resolve to: tenant-${tenantId}:env-${envId}.

Actual

Within the DO, this.ctx.id.name resolves to undefined

Workaround

Expose a setName function on the DO, invoke it before forwarding the request to the stub.

// DurableObject
export class SunriseDurableObject extends DurableObject<Env> {
  name: string = '';

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
  }

  async setName(name: string): Promise<void> {
    this.name = name;
  }

  async fetch(request: Request): Promise<Response> {
    if (!this.name) {
      throw new Error('DurableObject name must be set before initialization');
    }
    ...
  }
}
// Worker
const name = 'foobar'; // tenant id
const id = env.SUNRISE_DURABLE_OBJECT.idFromName(name)
const stub = env.SUNRISE_DURABLE_OBJECT.get(id);
await stub.setName(name); // Set the tenant id
response = await stub.fetch(request);

mikechabot avatar Aug 22 '25 05:08 mikechabot

If it's helpful to others, here is the pattern we're using atm to get around this limitation. It's a bit of boilerplate, and an extra get/put to the DO storage on every initialization of a DO instance, but not TOO bad.

import { DurableObject, env } from 'cloudflare:workers';

export interface BaseDO<NameParts extends string[]> {
  getNameParts: () => NameParts;
  saveNameParts: (nameParts: NameParts) => Promise<void>;
}

// Can be whatever.. just an example. We use branded string types for our ids.
export type AgentDONameParts = [TOrganizationId, TAgentId];

// Join comes from type-fest package
export type AgentDOName = Join<AgentDONameParts, ':'>;

/**
 * Everywhere else in our codebase calls this to get a DO instance
 * and must not use env.AGENT_DO directly
 * we also make this async for every DO in order to preserve the option to
 * do async stuff on initialization, before the DO is used
 */
export const makeAgentDO = async (nameParts: AgentDONameParts) => {
  const doId = env.AGENT_DO.idFromName(nameParts.join(':'));
  const instance = env.AGENT_DO.get(doId);
  await instance.saveNameParts(nameParts);
  return instance;
};

export class AgentDO extends DurableObject<Env> implements BaseDO<AgentDONameParts> {
  #orgId!: TOrganizationId;
  #agentId!: TAgentId;
  
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);

    // We can't use `this.ctx.id.name` because it's not supported yet
    // https://github.com/cloudflare/workerd/issues/2240
    void ctx.blockConcurrencyWhile(async () => {
      const [orgId, agentId] = (await this.ctx.storage.get<AgentDONameParts>('nameParts')) ?? [];
      this.#orgId = orgId || this.#orgId;
      this.#agentId = agentId || this.#agentId;

        // This case occurs when the DO instance is being re-created
      if (this.#orgId && this.#agentId) {
        await this.#initialize();
      }
    });
  }

  async #initialize() {
    // Here is where any async initialization should go, NOT in the constructor
  }

  async saveNameParts(nameParts: AgentDONameParts) {
    // This case occurs when the DO instance is being created for the first time
    if (this.#orgId !== nameParts[0] || this.#agentId !== nameParts[1]) {
      this.#orgId = nameParts[0];
      this.#agentId = nameParts[1];
      void this.ctx.storage.put<AgentDONameParts>('nameParts', nameParts);
      await this.#initialize();
    }
  }

  getNameParts(): AgentDONameParts {
    return [this.#orgId, this.#agentId];
  }
}

marbemac avatar Aug 22 '25 15:08 marbemac

I am using Hono and Agents/Agents Chat. The web socket connections gave me issues with other approaches, so I went with injecting the DO ID as a header. You could also persist to this.ctx.storage in the DO or setup an RPC call as a first layer with the header as a fallback

// server/index.ts

app.use("/api/*", async (c, next) => {
  // Your logic to determine the DO name identifier
 const someId = "foo";

  // Initialize Durable Object
  const doId = c.env.MyDurableObject.idFromName(someId);
  const doStub = c.env.MyDurableObject.get(doId);

  // Inject into request headers
  const originalRequest = c.req.raw;
  const headers = new Headers(originalRequest.headers);
  headers.set("x-do-id", doId);

  // Replace request with modified headers
  const modifiedRequest = new Request(originalRequest, { headers });
  c.req.raw = modifiedRequest;

  return await next();
});
// server/durable-object.ts

import { getCurrentAgent } from "agents";
private async getDoId(): Promise<string | undefined> {
try {
      const { request } = getCurrentAgent();
      if (request) {
        const doIdHeader = request.headers.get("x-do-id");
        if (doIdHeader) {
          return doIdHeader;
        }
      }
    } catch (error) {
      console.warn(
        "[DurableObject:getDoId] Could not access request headers:",
        error,
      );
    }
  return undefined;
}

ataylorme avatar Aug 22 '25 16:08 ataylorme

By the way, for those in the thread struggling with this, the new-ish @cloudflare/actors uses this pattern to guarantee the name is available in DOs:

https://github.com/cloudflare/actors/blob/main/packages/core/src/index.ts#L106-L115

    /**
     * Static method to get an actor instance by ID
     * @param id - The ID of the actor to get
     * @returns The actor instance
     */
    static get<T extends Actor<any>>(this: new (state: ActorState, env: any) => T, id: string): DurableObjectStub<T> {
        const stub = getActor(this, id);

        stub.setIdentifier(id);

        return stub;
    }

So they just call setIdentifier every time they "get" an Actor instance (which is a Durable Object) and refer to this.identifier rather than try to parse the name out of the id. It's very little code and works the same in dev and production.

grrowl avatar Aug 24 '25 06:08 grrowl

By the way, for those in the thread struggling with this, the new-ish @cloudflare/actors uses this pattern to guarantee the name is available in DOs:

https://github.com/cloudflare/actors/blob/main/packages/core/src/index.ts#L106-L115

/**
 * Static method to get an actor instance by ID
 * @param id - The ID of the actor to get
 * @returns The actor instance
 */
static get<T extends Actor<any>>(this: new (state: ActorState, env: any) => T, id: string): DurableObjectStub<T> {
    const stub = getActor(this, id);

    stub.setIdentifier(id);

    return stub;
}

So they just call setIdentifier every time they "get" an Actor instance (which is a Durable Object) and refer to this.identifier rather than try to parse the name out of the id. It's very little code and works the same in dev and production.

What is odd, their examples show that this should also be available in blockConcurrencyWhile yet, in reality it does not work, which is very odd. So if you do need this to work inside the constructor, it won't work

zekronium avatar Aug 31 '25 14:08 zekronium

I ran into this issue today. I have an agent that has a few tools and uses the scheduling feature: https://developers.cloudflare.com/agents/api-reference/schedule-tasks/ When I access the agent as usual through getAgentByName(), the getCurrentAgent() call in the tool function returns an agent with name properly. However when the agent executes a tool from a scheduled task instead, getCurrentAgent() returns an agent without name. When I try to access the agent name, it throws: "Attempting to read .name on Bot before it was set. The name can be set by explicitly calling .setName(name) on the stub, or by using routePartyKitRequest(). This is a known issue and will be fixed soon. Follow https://github.com/cloudflare/workerd/issues/2240 for more updates.". I'm thinking of saving the agent name in its state and using that instead or is there a better way?

abegehr avatar Nov 21 '25 12:11 abegehr