cloudflare / cloudflare/agents
Nested facet WebSocket routes recurse until the facet depth limit is exceeded
- 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
Assessment
This issue has not been assessed yet.