microsoft/playwright · error · Error

Unknown stream: ${params.streamId}

Error message

Unknown stream: ${params.streamId}

What it means

Thrown by the dashboard controller's `readStream` when `params.streamId` is not present in `_streams`. Streams are registered when produced (e.g. by `stopRecording`) and removed once EOF is returned or the handle is closed, so a second read after EOF, a stale id, or an invalid id all miss the map.

Source

Thrown at packages/playwright-core/src/tools/dashboard/dashboardController.ts:259

  async reveal(params: { path: string }) {
    switch (os.platform()) {
      case 'darwin':
        execFile('open', ['-R', params.path]);
        break;
      case 'win32':
        execFile('explorer', ['/select,', params.path]);
        break;
      case 'linux':
        execFile('xdg-open', [path.dirname(params.path)]);
        break;
    }
  }

  async readStream(params: { streamId: string }): Promise<{ data: string; eof: boolean }> {
    const stream = this._streams.get(params.streamId);
    if (!stream)
      throw new Error(`Unknown stream: ${params.streamId}`);
    const buffer = Buffer.alloc(256 * 1024);
    const { bytesRead } = await stream.handle.read(buffer, 0, buffer.length);
    if (bytesRead === 0) {
      this._streams.delete(params.streamId);
      await stream.handle.close().catch(() => {});
      await fs.promises.unlink(stream.path).catch(() => {});
      return { data: '', eof: true };
    }
    return { data: buffer.subarray(0, bytesRead).toString('base64'), eof: false };
  }

  visible(): boolean {
    return this._visible;
  }

  emitSessions(sessions: BrowserDescriptor[]) {
    this.sendEvent?.('sessions', { sessions, clientInfo: createClientInfo() });
  }

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Stop reading a stream once you receive `eof: true` — that response consumes the entry.
  2. Track the lifecycle of stream ids client-side and discard them on EOF.
  3. If the id came from `stopRecording`, ensure you only read it once per recording.

Example fix

// before
let { data, eof } = await controller.readStream({ streamId });
// ... later, reuse same streamId ...
await controller.readStream({ streamId }); // throws
// after
let eof = false;
while (!eof) {
  const r = await controller.readStream({ streamId });
  data += r.data; eof = r.eof;
}
// do not reuse streamId after the loop
Defensive patterns

Strategy: validation

Validate before calling

// Track consumed streams client-side.
const consumed = new Set<string>();
async function readFully(streamId: string) {
  if (consumed.has(streamId)) throw new Error(`stream already consumed: ${streamId}`);
  let data = '', eof = false;
  while (!eof) {
    const r = await controller.readStream({ streamId });
    data += r.data; eof = r.eof;
  }
  consumed.add(streamId);
  return data;
}

Prevention

When it happens

Trigger: Calling `readStream` with a `streamId` that was already consumed to EOF (the read that returns `eof:true` also deletes the entry), with a typo'd/foreign id, or before any stream was opened.

Common situations: Dashboard UI re-requests a recording after the download completed; a reconnecting dashboard client replaying an old stream id; concurrent reads racing past EOF.

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/661962c5cb8463f0. Report an issue: GitHub.