cloudflare / cloudflare/capnweb
ReadableStream cannot be returned through a forwarded stub
- 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
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