cloudflare / cloudflare/workerd

WorkerEntrypoint RPC crashes (wallTime=0) when worker co-exports a DurableObject class

Open
#6,453 3 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
8.7k
Forks
739
Avg merge
2d 20h
Merged PRs (30d)
174

Description

## Environment

- **wrangler**: 4.77.0
- **compatibility_date**: 2025-09-15
- **compatibility_flags**: `["nodejs_compat"]`
- **Platform**: Cloudflare Workers (staging deployment)

## Summary

When a Worker exports both a `WorkerEntrypoint` subclass and a `DurableObject` subclass, any RPC call to the `WorkerEntrypoint` crashes immediately with `wallTime=0 cpuTime=0 outcome=exception/canceled`. The same Worker responds correctly when invoked via HTTP `fetch()`.

**This is NOT specific to any one package.** We have confirmed the crash with two completely independent packages:

| Worker | Package | Depends on containers? | RPC Crash? |
|--------|---------|----------------------|------------|
| sandbox | `@cloudflare/sandbox` v0.8.0 | Yes | wallTime=0, outcome=canceled |
| browsing | `@cloudflare/puppeteer` v0.0.15 | **No** | wallTime=0, outcome=exception |

The common pattern: **WorkerEntrypoint + co-exported DurableObject class + RPC invocation**.

## Confirmed Staging Log Evidence

### Case 1: @cloudflare/sandbox — RPC crashes, HTTP works

**RPC call (crashes):**
```json
{
"entrypoint": "SandboxService",
"rpcMethod": "exec",
"wallTime": 0,
"cpuTime": 0,
"outcome": "canceled",
"scriptName": "cccc-sandbox-staging"
}
```

**HTTP fetch to same worker (works):**
```json
{
"event": { "request": { "url": ".../health", "method": "GET" } },
"wallTime": 1,
"outcome": "ok",
"scriptName": "cccc-sandbox-staging"
}
```

### Case 2: @cloudflare/puppeteer — Same crash, no containers dependency

```json
{
"entrypoint": "BrowsingService",
"rpcMethod": "navigate",
"wallTime": 0,
"cpuTime": 0,
"outcome": "exception",
"scriptName": "cccc-browsing-staging"
}
```

`@cloudflare/puppeteer` has zero dependency on `@cloudflare/containers`. Its dependencies are: `chromium-bidi`, `cross-fetch`, `debug`, `devtools-protocol`, `ws`, `@puppeteer/browsers`. This proves the crash is in workerd's RPC module initialization, not in any specific package.

## Steps to Reproduce

### Minimal setup

**Worker B** (target) — exports both a DO and a WorkerEntrypoint:

```ts
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";

// Any DurableObject subclass
export class MyDO extends DurableObject {
async sayHello() { return "hello from DO"; }
}

// WorkerEntrypoint that other workers call via RPC
export default class MyService extends WorkerEntrypoint {
async greet(name: string): Promise {
return `Hello, ${name}!`; // Never executes — crash happens before this
}
}
```

```toml
# wrangler.toml
name = "worker-b"
main = "src/index.ts"
compatibility_date = "2025-09-15"

[[durable_objects.bindings]]
name = "MY_DO"
class_name = "MyDO"

[[migrations]]
tag = "v1"
new_classes = ["MyDO"]
```

**Worker A** (caller) — calls Worker B via RPC:

```ts
export default {
async fetch(request: Request, env: any): Promise {
const result = await env.WORKER_B.greet("world");
return new Response(result);
},
};
```

```toml
# wrangler.toml
name = "worker-a"
main = "src/index.ts"

[[services]]
binding = "WORKER_B"
service = "worker-b"
entrypoint = "MyService"
```

### What happens

1. Worker A sends RPC call to `env.WORKER_B.greet("world")`
2. Worker B crashes: `wallTime=0 cpuTime=0 outcome=exception`
3. Worker A receives opaque internal error
4. No user code in Worker B executes

### Contrast: HTTP fetch works

`env.WORKER_B.fetch(new Request("https://fake/health"))` succeeds. The crash is specific to the RPC invocation path.

## Root Cause Analysis

The crash happens during **workerd's module initialization for RPC dispatch**. When workerd receives an RPC call targeting a `WorkerEntrypoint`, it evaluates the target Worker's module graph to discover and validate exported classes. This evaluation triggers something in the DurableObject subclass definitions that crashes in the RPC initialization context.

Key observations:
- `wallTime=0` confirms crash is during module init, not method execution
- The crash occurs regardless of which `WorkerEntrypoint` method is called
- The crash occurs even if the DO class is never instantiated or referenced by the entrypoint
- HTTP fetch uses a different init path that doesn't perform the same class validation
- Two unrelated packages (`@cloudflare/sandbox`, `@cloudflare/puppeteer`) both trigger it, ruling out package-specific causes

**The mere presence of a DurableObject subclass export in the same module graph as a WorkerEntrypoint is sufficient to trigger the crash.**

## Expected Behavior

RPC calls to a `WorkerEntrypoint` should succeed regardless of what other classes (including `DurableObject` subclasses) are exported from the same module. Class definitions that are not being instantiated should be inert during module evaluation.

## Workaround (Confirmed Working)

Convert the affected Worker from `WorkerEntrypoint` RPC to plain `export default { fetch() {} }` with HTTP fetch routing. The calling Worker uses `env.BINDING.fetch()` with JSON payloads instead of RPC.

```ts
// Instead of WorkerEntrypoint RPC:
export default class MyService extends WorkerEntrypoint { ... }

// Use plain module export with fetch routing:
export default {
async fetch(request: Request, env: Env): Promise {
const { method, args } = await request.json();
// Route to handlers based on method...
},
};
```

This works but loses RPC's type safety, requires manual JSON serialization, and adds unnecessary complexity.

## Impact

This bug effectively makes **WorkerEntrypoint RPC unusable for any Worker that also uses Durable Objects**. Since DO-based Workers are a core Cloudflare pattern (Containers, Browser Rendering, custom DOs), this forces a large class of Workers into the HTTP fetch workaround, negating the benefits of the RPC API.

## Affected Packages (confirmed)

- `@cloudflare/sandbox` v0.8.0 (exports `Sandbox extends Container extends DurableObject`)
- `@cloudflare/puppeteer` v0.0.15 (exports `BrowserSessionDO extends DurableObject` pattern)
- Likely any Worker that co-exports `WorkerEntrypoint` + `DurableObject` subclasses

## Related

- Workers RPC docs: https://developers.cloudflare.com/workers/runtime-apis/rpc/
- Durable Objects docs: https://developers.cloudflare.com/durable-objects/
- Cloudflare Containers: https://developers.cloudflare.com/containers/
- Browser Rendering API: https://developers.cloudflare.com/browser-rendering/
- Related issue: #4499 (DO RPC method override issue — different but adjacent)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.