Skip to content
Merged
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
2 changes: 1 addition & 1 deletion fixtures/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"dependencies": {
"nanoid": "^5.1.6",
"partyserver": "^0.1.4",
"partyserver": "^0.1.5",
"partysocket": "^1.1.13",
"react": "^19.2.3",
"react-dom": "^19.2.3"
Expand Down
2 changes: 1 addition & 1 deletion fixtures/globe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"dependencies": {
"cobe": "^0.6.5",
"partyserver": "^0.1.4",
"partyserver": "^0.1.5",
"partysocket": "^1.1.13",
"react": "^19.2.3",
"react-dom": "^19.2.3"
Expand Down
2 changes: 1 addition & 1 deletion fixtures/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"dependencies": {
"hono": "^4.11.1",
"hono-party": "^1.0.0",
"partyserver": "^0.1.4",
"partyserver": "^0.1.5",
"partysocket": "^1.1.13",
"react": "^19.2.3",
"react-dom": "^19.2.3"
Expand Down
2 changes: 1 addition & 1 deletion fixtures/tiptap-yjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@tiptap/extension-collaboration": "^3.14.0",
"@tiptap/react": "^3.14.0",
"@tiptap/starter-kit": "^3.14.0",
"partyserver": "^0.1.4",
"partyserver": "^0.1.5",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"tailwindcss": "^4.1.18",
Expand Down
2 changes: 1 addition & 1 deletion fixtures/tldraw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"start": "vite dev"
},
"dependencies": {
"partyserver": "^0.1.4",
"partyserver": "^0.1.5",
"partysocket": "^1.1.13",
"react": "^19.2.3",
"react-dom": "^19.2.3",
Expand Down
2 changes: 1 addition & 1 deletion fixtures/todo-sync/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"dependencies": {
"nanoid": "^5.1.6",
"partyserver": "^0.1.4",
"partyserver": "^0.1.5",
"partysocket": "^1.1.13",
"react": "^19.2.3",
"react-dom": "^19.2.3",
Expand Down
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/hono-party/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@
"devDependencies": {
"@cloudflare/workers-types": "^4.20251218.0",
"hono": "^4.11.1",
"partyserver": "^0.1.3"
"partyserver": "^0.1.4"
}
}
2 changes: 1 addition & 1 deletion packages/partyfn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
],
"dependencies": {
"nanoid": "^5.1.6",
"partysocket": "^1.1.12"
"partysocket": "^1.1.13"
},
"scripts": {
"build": "tsx scripts/build.ts"
Expand Down
11 changes: 11 additions & 0 deletions packages/partyserver/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# partyflare

## 0.1.5

### Patch Changes

- [#323](https://github.com/cloudflare/partykit/pull/323) [`353da20`](https://github.com/cloudflare/partykit/commit/353da207f8d31ef406374159ee345292616ec1ca) Thanks [@threepointone](https://github.com/threepointone)! - Fix initialization race conditions and improve error resilience.
- `getServerByName` now propagates errors from the internal `set-name` request instead of silently swallowing them.
- `onStart` failures no longer permanently brick the Durable Object. Errors are caught inside `blockConcurrencyWhile` (preserving the input gate) and the status is reset, allowing subsequent requests to retry initialization.
- `fetch()` now retries initialization when a previous `onStart` attempt failed, instead of skipping it because the name was already set.
- Errors in `fetch()` (including `onStart` failures and malformed props) are now caught and returned as proper 500 responses instead of crashing as unhandled exceptions.
- WebSocket handlers (`webSocketMessage`, `webSocketClose`, `webSocketError`) are now wrapped in try/catch so that transient `onStart` failures don't kill the connection — the next message will retry.

## 0.1.4

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/partyserver/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "partyserver",
"version": "0.1.4",
"version": "0.1.5",
"repository": {
"type": "git",
"url": "git://github.com/cloudflare/partykit.git"
Expand Down
118 changes: 70 additions & 48 deletions packages/partyserver/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,7 @@ export async function getServerByName<
}

// unfortunately we have to await this
await stub
.fetch(req)
// drain body
.then((res) => res.text())
.catch((e) => {
console.error("Could not set server name:", e);
});
await stub.fetch(req).then((res) => res.text());

return stub;
}
Expand Down Expand Up @@ -366,34 +360,32 @@ export class Server<
* Handle incoming requests to the server.
*/
async fetch(request: Request): Promise<Response> {
// Set the props in-mem if the request included them.
const props = request.headers.get("x-partykit-props");
if (props) {
try {
try {
// Set the props in-mem if the request included them.
const props = request.headers.get("x-partykit-props");
if (props) {
this.#_props = JSON.parse(props);
} catch {
// This should never happen but log it just in case
console.error("Internal error parsing context props.");
}
}
if (!this.#_name) {
// This is temporary while we solve https://github.com/cloudflare/workerd/issues/2240

if (!this.#_name) {
// This is temporary while we solve https://github.com/cloudflare/workerd/issues/2240

// get namespace and room from headers
// const namespace = request.headers.get("x-partykit-namespace");
const room = request.headers.get("x-partykit-room");
if (
// !namespace ||
!room
) {
throw new Error(`Missing namespace or room headers when connecting to ${this.#ParentClass.name}.
// get namespace and room from headers
// const namespace = request.headers.get("x-partykit-namespace");
const room = request.headers.get("x-partykit-room");
if (
// !namespace ||
!room
) {
throw new Error(`Missing namespace or room headers when connecting to ${this.#ParentClass.name}.
Did you try connecting directly to this Durable Object? Try using getServerByName(namespace, id) instead.`);
}
await this.setName(room);
} else if (this.#status !== "started") {
// Name was set by a previous request but initialization failed.
// Retry initialization so the server can recover from transient
// onStart failures.
await this.#initialize();
}
await this.setName(room);
}

try {
const url = new URL(request.url);

// TODO: this is a hack to set the server name,
Expand Down Expand Up @@ -450,7 +442,7 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam
}
} catch (err) {
console.error(
`Error in ${this.#ParentClass.name}:${this.name} fetch:`,
`Error in ${this.#ParentClass.name}:${this.#_name ?? "<unnamed>"} fetch:`,
err
);
if (!(err instanceof Error)) throw err;
Expand All @@ -477,13 +469,20 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam
return;
}

const connection = createLazyConnection(ws);
try {
const connection = createLazyConnection(ws);

// rehydrate the server name if it's woken up
await this.setName(connection.server);
// TODO: ^ this shouldn't be async
// rehydrate the server name if it's woken up
await this.setName(connection.server);
// TODO: ^ this shouldn't be async

return this.onMessage(connection, message);
return this.onMessage(connection, message);
} catch (e) {
console.error(
`Error in ${this.#ParentClass.name}:${this.#_name ?? "<unnamed>"} webSocketMessage:`,
e
);
}
}

async webSocketClose(
Expand All @@ -496,35 +495,58 @@ Did you try connecting directly to this Durable Object? Try using getServerByNam
return;
}

const connection = createLazyConnection(ws);
try {
const connection = createLazyConnection(ws);

// rehydrate the server name if it's woken up
await this.setName(connection.server);
// TODO: ^ this shouldn't be async
// rehydrate the server name if it's woken up
await this.setName(connection.server);
// TODO: ^ this shouldn't be async

return this.onClose(connection, code, reason, wasClean);
return this.onClose(connection, code, reason, wasClean);
} catch (e) {
console.error(
`Error in ${this.#ParentClass.name}:${this.#_name ?? "<unnamed>"} webSocketClose:`,
e
);
}
}

async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
if (!isPartyServerWebSocket(ws)) {
return;
}

const connection = createLazyConnection(ws);
try {
const connection = createLazyConnection(ws);

// rehydrate the server name if it's woken up
await this.setName(connection.server);
// TODO: ^ this shouldn't be async
// rehydrate the server name if it's woken up
await this.setName(connection.server);
// TODO: ^ this shouldn't be async

return this.onError(connection, error);
return this.onError(connection, error);
} catch (e) {
console.error(
`Error in ${this.#ParentClass.name}:${this.#_name ?? "<unnamed>"} webSocketError:`,
e
);
}
}

async #initialize(): Promise<void> {
let error: unknown;
await this.ctx.blockConcurrencyWhile(async () => {
this.#status = "starting";
await this.onStart(this.#_props);
this.#status = "started";
try {
await this.onStart(this.#_props);
this.#status = "started";
} catch (e) {
this.#status = "zero";
error = e;
}
});
// Re-throw outside blockConcurrencyWhile so the DO's input gate
// isn't permanently broken, allowing subsequent requests to retry.
if (error) throw error;
}

#attachSocketEventHandlers(connection: Connection) {
Expand Down
Loading
Loading