cloudflare / cloudflare/capnweb

ReadableStream cannot be returned through a forwarded stub

Open
#228 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
4k
Forks
143
Avg merge
4d 6h
Merged PRs (30d)
7

Description

A ReadableStream returned by an RPC method works when the caller holds a direct session to the target. It throws when the call is forwarded through an intermediate session, such as when a broker returns a dup of a stub it holds to a third party.

1. Create a session between a broker and a child. The child has a method that returns a ReadableStream.
2. Create a second session between a caller and the broker. The broker exposes a method that returns `childStub.dup()`.
3. From the caller, resolve that stub and call the stream-returning method on it.

Expected: the caller receives a ReadableStream, the same as it would on a direct session.

Actual: `Error: owned payload shouldn't contain raw ReadableStreams`.

Cause: `RpcPayload.ensureDeepCopied()` sets `source` to `"owned"` after the first hop. `getHookForReadableStream` handles `"params"` and `"return"` and throws on anything else. Regular stubs forward across the same two sessions without error, so only streams hit this.

```js
import { newMessagePortRpcSession, RpcTarget } from "capnweb";

class Child extends RpcTarget {
async ping() { return "pong"; }
async tail() {
let i = 0;
return new ReadableStream({
pull(c) { if (i >= 3) return c.close(); c.enqueue(new Uint8Array([i++])); }
});
}
}

class Broker extends RpcTarget {
constructor(child) { super(); this.child = child; }
async resolveChild() { return this.child.dup(); }
}

const childChan = new MessageChannel();
newMessagePortRpcSession(childChan.port2, new Child());
const childFromBroker = newMessagePortRpcSession(childChan.port1);

const brokerChan = new MessageChannel();
newMessagePortRpcSession(brokerChan.port2, new Broker(childFromBroker));
const broker = newMessagePortRpcSession(brokerChan.port1);

const child = await broker.resolveChild();
console.log(await child.ping()); // "pong"
await child.tail(); // throws
```

Tested on capnweb 0.10.0. The same error string is present in 0.8.0.

Workaround: return an RpcTarget with a `next()` method instead of the stream, and rebuild the stream on the caller side.

Contributor guide

Open the contributing guide

Research direction

Start with RpcPayload.ensureDeepCopied() and getHookForReadableStream, then reproduce the two-session broker example from the issue. Verify that a ReadableStream returned through childStub.dup() reaches the caller without the owned-payload error, while direct-session behavior remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
backend-api-design, distributed-systems
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.