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
- Stop reading a stream once you receive `eof: true` — that response consumes the entry.
- Track the lifecycle of stream ids client-side and discard them on EOF.
- 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
- Never call readStream after receiving eof:true (that entry is gone).
- Discard stream ids client-side once consumed.
- Bind each read sequence to a single stream id obtained from stopRecording.
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
- No recording in progress
- The object has been collected to prevent unbounded heap grow
- Cannot find object to "${method}": ${guid}
- Unknown new child: ${params.guid}
- Cannot find parent object ${parentGuid} to create ${guid}
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/661962c5cb8463f0.
Report an issue: GitHub.