cloudflare / cloudflare/agents

Nested facet WebSocket routes recurse until the facet depth limit is exceeded

Open
#2,026 1 comment 0 reactions 1 assignee Claimed by @threepointone View on GitHub
bug
Dominant language
TypeScript
Stars
5.6k
Forks
711
Avg merge
1d 20h
Merged PRs (30d)
53

Description

**Describe the bug**

A WebSocket addressed through two nested facet hops upgrades successfully with HTTP 101, but fails during session setup and closes with code 1011.

For a route shaped like:

```text
root Agent
→ middle facet
→ leaf facet
```

the leaf reinterprets the full ancestor route as a route to one of its own children. Facets are then created recursively until workerd rejects the chain with:

```text
Facet nesting depth limit exceeded.
The maximum depth including the root Durable Object is 4.
```

**To Reproduce**

On current `main` (`2b2b5980e1945cf55f5a11626bc395e7c460516f`), the existing `SpikeSubParent` and `SpikeSubChild` fixtures are sufficient:

```ts
it("establishes a WebSocket through two nested facet hops", async () => {
const parent = uniqueName();
const middle = uniqueName();
const leaf = uniqueName();

const ws = await openWS(
parent,
"spike-sub-child",
middle,
`/sub/spike-sub-child/${leaf}`
);

ws.send("hello");
const [reply] = await collectMessages(ws, 1);

expect(reply).toBe(`pong:${leaf}:hello`);
expect(ws.readyState).toBe(WebSocket.OPEN);

ws.close();
});
```

Run:

```bash
cd packages/agents
pnpm exec vitest run --config src/tests/vitest.config.ts \
src/tests/spike-sub-agent-routing.test.ts \
-t "two nested facet hops"
```

The existing one-hop WebSocket test continues to pass.

**Observed behavior**

1. The WebSocket upgrade returns 101.
2. No leaf echo arrives.
3. The worker reports the facet nesting-depth error.
4. The socket closes with:
- code: `1011`
- reason: `Uncaught exception during session setup`
- `wasClean: true`

The failure reproduces with different middle and leaf facet classes as well; it is not specific to same-class nesting or callable methods.

**Expected behavior**

The nested route should establish a live WebSocket to the leaf facet, and the client should receive:

```text
pong:{leaf}:hello
```

**Version**

Current `main` at `2b2b5980e1945cf55f5a11626bc395e7c460516f`.

**Additional context**

The root WebSocket stores the complete nested route in the private `x-cf-agents-subagent-url` header.

`_cf_resolveSubAgentConnection()` correctly strips one `/sub/{class}/{name}` hop into `meta.uri`, but also copies the private full-route header into `meta.requestHeaders`. Each virtual descendant reconstructs its request with those headers.

When the leaf's wrapped `onConnect` runs, it stores the stale full ancestor URL on the leaf connection. Resolution prioritizes that URL over the already-stripped `meta.uri`, so the leaf treats the ancestor facet as a child and starts the recursive cycle.

A likely fix is to consume/remove `x-cf-agents-subagent-url` before propagating request headers into virtual descendant connection metadata. The root-owned physical socket has already persisted the full route; descendants can route using their stripped `meta.uri`.

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.