ReadableStream cannot be returned through a forwarded stub

Open
#228 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
68/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Quiet

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.

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.

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.

Dominant language
TypeScript
Stars
4k
Forks
151
Avg merge
16h 46m
Merged PRs (30d)
2

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from cloudflare/capnweb

All issues in cloudflare/capnweb

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.