Skip to content

Commit 79d5c6b

Browse files
committed
Fix ordering: apply x-partykit-room header before ensureInitialized
The first pass of PR #381 ran #ensureInitialized() BEFORE applying the x-partykit-room header fallback. For DOs with ctx.id.name undefined and no __ps_name storage record, that meant onStart() ran against an unset #_name and threw if it read this.name. Restore the 0.5.0 ordering: pre-populate #_name from the header before ensureInitialized runs, so onStart() sees the correct name regardless of which source provided it. The resolvability throw is now a final check after ensureInitialized. Resolution priority is now: ctx.id.name > x-partykit-room header > legacy __ps_name storage. Storage only wins when neither of the other two supplies the name. Adds HeaderOnlyOnStartServer + a regression test covering the newUniqueId + x-partykit-room + onStart-reads-name path. Made-with: Cursor
1 parent b8890bc commit 79d5c6b

4 files changed

Lines changed: 64 additions & 12 deletions

File tree

packages/partyserver/src/index.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -389,24 +389,24 @@ export class Server<
389389
this.#_props = JSON.parse(props);
390390
}
391391

392-
// Name resolution in `#ensureInitialized()` consults
393-
// `ctx.id.name`, then a legacy storage record, so by the time it
394-
// returns `this.name` is populated for any properly-addressed
395-
// DO or any DO bootstrapped via the legacy `__ps_name` storage
396-
// record (e.g. Cloudflare Agents facets). The only remaining
397-
// case is a caller that bypasses both — then we fall back to
398-
// the `x-partykit-room` request header.
392+
// Name resolution priority: ctx.id.name > x-partykit-room header
393+
// > legacy __ps_name storage record. Pre-populate from the header
394+
// BEFORE `#ensureInitialized()` so that `onStart()` sees the name
395+
// regardless of how it was supplied. `#ensureInitialized()` will
396+
// fall back to reading storage when neither ctx.id.name nor the
397+
// header has provided one.
398+
if (!this.ctx.id.name && !this.#_name) {
399+
const room = request.headers.get("x-partykit-room");
400+
if (room) this.#_name = room;
401+
}
402+
399403
await this.#ensureInitialized();
400404

401405
if (!this.ctx.id.name && !this.#_name) {
402-
const room = request.headers.get("x-partykit-room");
403-
if (!room) {
404-
throw new Error(`Cannot determine the name for ${this.#ParentClass.name}: this.ctx.id.name is undefined, no legacy __ps_name storage record is present, and no x-partykit-room header was supplied. Likely causes:
406+
throw new Error(`Cannot determine the name for ${this.#ParentClass.name}: this.ctx.id.name is undefined, no legacy __ps_name storage record is present, and no x-partykit-room header was supplied. Likely causes:
405407
1. The stub was built via idFromString()/newUniqueId(). PartyServer requires name-based addressing (idFromName/getByName).
406408
2. The workerd/wrangler runtime is too old to expose ctx.id.name — update to a recent wrangler release.
407409
3. You called stub.fetch() directly without going through routePartykitRequest()/getServerByName(). Prefer those, or set the x-partykit-room header.`);
408-
}
409-
this.#_name = room;
410410
}
411411

412412
const url = new URL(request.url);

packages/partyserver/src/tests/index.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,29 @@ describe("this.name in the constructor", () => {
699699
});
700700
});
701701

702+
describe("x-partykit-room header applied before onStart", () => {
703+
it("populates this.name from header before onStart runs", async () => {
704+
// Regression guard: for DOs with `ctx.id.name === undefined` where
705+
// the caller supplies the name via `x-partykit-room`, the header
706+
// must be applied BEFORE `#ensureInitialized()` runs `onStart()`.
707+
// Otherwise an onStart that reads `this.name` would throw.
708+
const id = env.HeaderOnlyOnStartServer.newUniqueId();
709+
const stub = env.HeaderOnlyOnStartServer.get(id);
710+
const res = await stub.fetch(
711+
new Request("http://example.com/", {
712+
headers: { "x-partykit-room": "header-onstart-test" }
713+
})
714+
);
715+
expect(res.status).toBe(200);
716+
const data = (await res.json()) as {
717+
name: string;
718+
onStartName: string | null;
719+
};
720+
expect(data.name).toBe("header-onstart-test");
721+
expect(data.onStartName).toBe("header-onstart-test");
722+
});
723+
});
724+
702725
describe("Framework bootstrap fallback (Agents facets etc.)", () => {
703726
it("hydrates this.name from __ps_name storage when ctx.id.name is undefined", async () => {
704727
// Regression guard for Cloudflare Agents facets. Facets are spawned

packages/partyserver/src/tests/worker.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type Env = {
1515
AlarmServer: DurableObjectNamespace<AlarmServer>;
1616
AlarmNameServer: DurableObjectNamespace<AlarmNameServer>;
1717
NoNameServer: DurableObjectNamespace<NoNameServer>;
18+
HeaderOnlyOnStartServer: DurableObjectNamespace<HeaderOnlyOnStartServer>;
1819
FacetLikeBootstrapServer: DurableObjectNamespace<FacetLikeBootstrapServer>;
1920
NameInConstructorServer: DurableObjectNamespace<NameInConstructorServer>;
2021
Mixed: DurableObjectNamespace<Mixed>;
@@ -257,6 +258,29 @@ export class NoNameServer extends Server {
257258
}
258259
}
259260

261+
/**
262+
* Regression guard: DO with `ctx.id.name === undefined` (because it was
263+
* addressed via `newUniqueId()`) whose `onStart()` reads `this.name`.
264+
* When the caller supplies the name via the `x-partykit-room` header,
265+
* `Server.fetch()` must apply the header BEFORE running `onStart()`, so
266+
* `this.name` is readable during `onStart()`.
267+
*/
268+
export class HeaderOnlyOnStartServer extends Server {
269+
onStartName: string | null = null;
270+
271+
async onStart() {
272+
// Throws if `this.name` isn't resolvable here.
273+
this.onStartName = this.name;
274+
}
275+
276+
onRequest(): Response {
277+
return Response.json({
278+
name: this.name,
279+
onStartName: this.onStartName
280+
});
281+
}
282+
}
283+
260284
/**
261285
* Simulates a framework-level bootstrap pattern (e.g. Cloudflare Agents
262286
* facets) where the DO is NOT addressed via `idFromName()` — so

packages/partyserver/src/tests/wrangler.jsonc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@
8787
"name": "PropsServer",
8888
"class_name": "PropsServer"
8989
},
90+
{
91+
"name": "HeaderOnlyOnStartServer",
92+
"class_name": "HeaderOnlyOnStartServer"
93+
},
9094
{
9195
"name": "FacetLikeBootstrapServer",
9296
"class_name": "FacetLikeBootstrapServer"
@@ -120,6 +124,7 @@
120124
"UriServer",
121125
"UriServerInMemory",
122126
"PropsServer",
127+
"HeaderOnlyOnStartServer",
123128
"FacetLikeBootstrapServer",
124129
"NameInConstructorServer"
125130
]

0 commit comments

Comments
 (0)