Skip to content

Custom routing

Last updated View as MarkdownAgent setup

routeAgentRequest() is optional. Any request that reaches a Lifecycle Durable Object through fetch() goes through the Lifecycle, however your Worker found the object. Use custom routing when you want your own URLs, or when the server, not the client, decides which object a request goes to.

Forward a request to a named object

Get a stub by name and call fetch():

src/index.jsjs
export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		// /chat/:room goes to the ChatRoom object named :room.
		const room = url.pathname.match(/^\/chat\/([^/]+)/)?.[1];
		if (room) {
			return env.ChatRoom.getByName(decodeURIComponent(room)).fetch(request);
		}

		return new Response("Not found", { status: 404 });
	},
};
src/index.tsts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		// /chat/:room goes to the ChatRoom object named :room.
		const room = url.pathname.match(/^\/chat\/([^/]+)/)?.[1];
		if (room) {
			return env.ChatRoom.getByName(decodeURIComponent(room)).fetch(request);
		}

		return new Response("Not found", { status: 404 });
	},
} satisfies ExportedHandler<Env>;

The forwarded request starts the object if it is cold, then reaches its capabilities and onRequest(). WebSocket upgrades work the same way, so the WebSockets capability accepts them.

For placement, address the object the same way routeAgentRequest() does:

const namespace = env.ChatRoom.jurisdiction("eu");
const stub = namespace.get(namespace.idFromName("general"), {
	locationHint: "weur",
});
const response = await stub.fetch(request);
const namespace = env.ChatRoom.jurisdiction("eu");
const stub = namespace.get(namespace.idFromName("general"), {
	locationHint: "weur",
});
const response = await stub.fetch(request);

The props routing option only works through routeAgentRequest(). For a forwarded request, send the values with the request, or call the object over RPC.

Choose the object on the server

When the object depends on who is calling, choose its name from the session. The client connects to a fixed path and never sees or sends the name:

src/index.jsjs
import { routeAgentRequest } from "agents/routing";

async function getUserId(request) {
	// Read the user from your session cookie or token.
	return request.headers.get("x-user-id");
}

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (url.pathname === "/me") {
			const userId = await getUserId(request);
			if (!userId) return new Response("Unauthorized", { status: 401 });
			return env.UserAgent.getByName(userId).fetch(request);
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
};
src/index.tsts
import { routeAgentRequest } from "agents/routing";

async function getUserId(request: Request): Promise<string | null> {
	// Read the user from your session cookie or token.
	return request.headers.get("x-user-id");
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		if (url.pathname === "/me") {
			const userId = await getUserId(request);
			if (!userId) return new Response("Unauthorized", { status: 401 });
			return env.UserAgent.getByName(userId).fetch(request);
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

Connect the client to that path with basePath. The agent option is still required, but it is not used to build the URL:

import { useAgent } from "agents/react";

export function Profile() {
	const agent = useAgent({
		agent: "UserAgent",
		basePath: "me",
		onIdentity: (name, agentType) => {
			console.log(`Connected to ${agentType} ${name}`);
		},
	});

	return <p>{agent.identified ? agent.name : "Connecting..."}</p>;
}

The WebSockets capability sends an identity frame when a client connects, so agent.name and onIdentity report the name the server chose. If the identity changes when the client reconnects, for example after the user signs in as someone else, onIdentityChange runs.

The identity frame includes the object's name. If the name is private, such as an internal user ID, do not send it. Set protocol: false on WebSockets, and send your own frames from onConnect. For more information, refer to Control the protocol per connection.

Call an object over RPC

Public methods on a Durable Object are RPC methods. Call them from your Worker, or from another Durable Object, with a stub:

src/index.jsjs
import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
import { Scheduler } from "agents/schedules";

export class UserAgent extends DurableObject {
	scheduler = new Scheduler({ callbacks: {} });
	lifecycle = Lifecycle.install(this).use(this.scheduler);

	async pendingReminders(props) {
		await this.lifecycle.start(props);
		return (await this.scheduler.list()).length;
	}
}

export default {
	async fetch(request, env) {
		const agents = env.UserAgent;
		const count = await agents
			.getByName("alice")
			.pendingReminders({ userId: "alice" });
		return Response.json({ count });
	},
};
src/index.tsts
import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
import { Scheduler } from "agents/schedules";

type Props = { userId: string };

export class UserAgent extends DurableObject<Env> {
	readonly scheduler = new Scheduler({ callbacks: {} });
	readonly lifecycle = Lifecycle.install<Env, Props>(this).use(this.scheduler);

	async pendingReminders(props?: Props): Promise<number> {
		await this.lifecycle.start(props);
		return (await this.scheduler.list()).length;
	}
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const agents = env.UserAgent as DurableObjectNamespace<UserAgent>;
		const count = await agents
			.getByName("alice")
			.pendingReminders({ userId: "alice" });
		return Response.json({ count });
	},
} satisfies ExportedHandler<Env>;

RPC calls do not go through fetch(), so they do not start the Lifecycle. Call await this.lifecycle.start() at the top of each RPC method that uses a capability, or anything onStart() sets up.

The props you pass to start() only apply if this call starts the object. For more information, refer to Start the Lifecycle.

getAgentByName() from agents starts the object for you, but it only works with the Agent class. On a plain Durable Object, use getByName() and lifecycle.start().

Was this helpful?