Skip to content

Commit 2913b78

Browse files
committed
Fix Actor configuration override for custom upgrade paths
1 parent 9ec15ae commit 2913b78

2 files changed

Lines changed: 74 additions & 5 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
vi.mock("cloudflare:workers", () => {
4+
class DurableObject {
5+
constructor(_state?: unknown, _env?: unknown) {}
6+
}
7+
8+
class WorkerEntrypoint {}
9+
10+
return {
11+
DurableObject,
12+
WorkerEntrypoint,
13+
env: {},
14+
};
15+
});
16+
17+
import { Actor, type ActorConfiguration } from "./index";
18+
19+
describe("Actor configuration overrides", () => {
20+
it("respects custom upgrade paths defined by subclasses", async () => {
21+
let upgradeCalls = 0;
22+
23+
class CustomPathActor extends Actor<Record<string, never>> {
24+
static override configuration(): ActorConfiguration {
25+
return {
26+
sockets: {
27+
upgradePath: "/custom",
28+
},
29+
};
30+
}
31+
32+
protected override async shouldUpgradeWebSocket(
33+
request: Request,
34+
): Promise<boolean> {
35+
return request.headers.get("Upgrade")?.toLowerCase() === "websocket";
36+
}
37+
38+
protected override onWebSocketUpgrade(_request: Request): Response {
39+
upgradeCalls += 1;
40+
return new Response("upgraded", { status: 200 });
41+
}
42+
43+
protected override onRequest(): Promise<Response> {
44+
return Promise.resolve(new Response("fallback", { status: 418 }));
45+
}
46+
}
47+
48+
const actor = new CustomPathActor(undefined, undefined);
49+
(actor as Record<string, unknown>)["_setNameCalled"] = true;
50+
51+
const upgradeResponse = await actor.fetch(
52+
new Request("https://example.com/custom/game", {
53+
headers: { Upgrade: "websocket" },
54+
}),
55+
);
56+
// Node/undici Response objects cannot emit 101, so we just ensure the response we returned flows through.
57+
expect(upgradeResponse.status).toBe(200);
58+
expect(upgradeCalls).toBe(1);
59+
60+
const fallbackResponse = await actor.fetch(
61+
new Request("https://example.com/ws/game"),
62+
);
63+
expect(fallbackResponse.status).toBe(418);
64+
expect(upgradeCalls).toBe(1);
65+
});
66+
});

packages/core/src/index.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,11 @@ export abstract class Actor<E> extends DurableObject<E> {
107107

108108
/**
109109
* Static method to configure the actor.
110-
* @param options
111-
* @returns
110+
* Subclasses can override this to customize upgrade paths or other options.
111+
* @param request - Incoming request (optional for subclasses that need context)
112+
* @returns Actor configuration values
112113
*/
113-
static configuration = (request: Request): ActorConfiguration => {
114+
static configuration(request?: Request): ActorConfiguration {
114115
return {
115116
locationHint: undefined,
116117
sockets: {
@@ -237,7 +238,8 @@ export abstract class Actor<E> extends DurableObject<E> {
237238
async fetch(request: Request): Promise<Response> {
238239
// If the request route is `/ws` then we should upgrade the connection to a WebSocket
239240
// Get configuration from the static property
240-
const config = (this.constructor as typeof Actor).configuration(request);
241+
const ActorClass = this.constructor as typeof Actor;
242+
const config = ActorClass.configuration(request);
241243

242244
// Parse the URL to check if the path component matches the upgradePath
243245
const url = new URL(request.url);
@@ -562,7 +564,8 @@ export function getActor<T extends Actor<any>>(
562564
): DurableObjectStub<T> {
563565
const className = ActorClass.name;
564566
const envObj = env as unknown as Record<string, DurableObjectNamespace>;
565-
const locationHint = (ActorClass as any).configuration().locationHint;
567+
const actorConfig = (ActorClass as unknown as typeof Actor).configuration();
568+
const locationHint = actorConfig.locationHint;
566569

567570
const bindingName = Object.keys(envObj).find(key => {
568571
const binding = (env as any).__DURABLE_OBJECT_BINDINGS?.[key];

0 commit comments

Comments
 (0)