[Bug]: WebKit emits pageerror for native RTCDataChannel send diagnostics without a JS exception

未关闭
#44,103 2 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
45/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃

调研方向

Start by comparing WebKit's RTCDataChannel.cpp diagnostic path with Playwright's _onConsoleMessage in packages/playwright-core/src/server/webkit/wkPage.ts; the issue links both locations and a related report, #43120. Reproduce with the supplied finite script and check whether native failed-send diagnostics can be distinguished from unhandled JavaScript exceptions without hiding real exceptions. Done means the diagnostic remains observable but is no longer emitted as a pageerror.

由索引模型根据 Issue 内容生成。

描述

agent-triage
Version and environment

Playwright 1.65.0, bundled WebKit revision 2388, Node v26.13.0, Linux x64, Ubuntu 24.04.5 LTS. The same diagnostic intermittently fails an actual three-peer WebRTC test on GitHub Actions.

Reproduction

Run the finite app-free script below with node rtc-handled-send.mjs after installing Playwright and its WebKit browser. It uses two local RTC peers, mock microphone permission/capture to permit host ICE candidates, no STUN server, no application accounts and two teardown sends. Readiness is bounded at ten seconds; all channels, capture tracks and the browser are closed.

// Three finite native sends: control, remote teardown, local teardown.
// Distinguish page JavaScript errors from WebKit's handled native diagnostic.
import assert from 'node:assert/strict';
import { webkit } from 'playwright';
const browser = await webkit.launch({ headless: true });
const diagnostics = [];
let phase = 'setup';
try {
  const context = await browser.newContext({ permissions: ['microphone'] });
  const page = await context.newPage();
  page.on('console', (event) => { if (event.text().startsWith('native-probe:')) phase = event.text().slice(13); });
  page.on('pageerror', (error) => diagnostics.push({ phase, name: error.name, message: error.message }));
  await page.route('https://rtc-fixture.invalid/', (route) => route.fulfill({ contentType: 'text/html', body: '<title>Native RTC send</title>' }));
  await page.goto('https://rtc-fixture.invalid/');
  const result = await page.evaluate(async () => {
    const errors = [];
    globalThis.addEventListener('error', (event) => errors.push(event.message));
    globalThis.addEventListener('unhandledrejection', (event) => errors.push(String(event.reason)));
    const media = await navigator.mediaDevices.getUserMedia({ audio: true });
    const a = new RTCPeerConnection({ iceServers: [] });
    const b = new RTCPeerConnection({ iceServers: [] });
    let timer;
    let incoming;
    try {
      a.onicecandidate = (event) => { if (event.candidate) void b.addIceCandidate(event.candidate).catch(() => {}); };
      b.onicecandidate = (event) => { if (event.candidate) void a.addIceCandidate(event.candidate).catch(() => {}); };
      const channel = a.createDataChannel('bounded-native-fixture');
      const ready = new Promise((resolve, reject) => {
        timer = setTimeout(() => reject(new Error('native fixture readiness')), 10000);
        b.ondatachannel = (event) => { incoming = event.channel; incoming.onmessage = () => resolve(); };
        channel.onopen = () => channel.send('control');
      });
      ready.catch(() => {});
      await a.setLocalDescription(await a.createOffer());
      await b.setRemoteDescription(a.localDescription);
      await b.setLocalDescription(await b.createAnswer());
      await a.setRemoteDescription(b.localDescription);
      await ready;
      const sends = [];
      const send = (phase) => {
        const entry = { phase, state: channel.readyState, connection: a.connectionState };
        try { channel.send('after teardown'); entry.sent = true; }
        catch (error) { entry.caught = { name: error.name, message: error.message }; }
        sends.push(entry);
      };
      console.log('native-probe:immediate-teardown');
      b.close();
      send('remote-close');
      console.log('native-probe:local-close');
      a.close();
      send('local-close');
      await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
      return { sends, errors };
    } finally {
      clearTimeout(timer);
      incoming?.close(); a.close(); b.close();
      media.getTracks().forEach((track) => track.stop());
    }
  });
  // Flush native protocol diagnostics without a timed sleep.
  await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
  console.log(JSON.stringify({ result, diagnostics }, null, 2));
  assert.deepEqual(result.errors, []);
} finally { await browser.close(); }
Observed

Both teardown channel.send() calls can return normally, while window.error and unhandledrejection remain empty. Playwright nevertheless emits pageerror with an empty error name and Error sending string through RTCDataChannel.. The last send can see channel.readyState === 'open' while its peer connection is already closed. This is a native asynchronous send diagnostic, not an escaped JavaScript exception. The current run emits the diagnostic for both teardown phases. A separate run waiting two animation frames between closes had no diagnostic and correctly caught InvalidStateError for the later closed channel.

WebKit's RTCDataChannel message queue logs a native transport failure using addConsoleMessage(MessageSource::JS, MessageLevel::Error, ...): https://github.com/WebKit/WebKit/blob/main/Source/WebCore/Modules/mediastream/RTCDataChannel.cpp#L72
Playwright's _onConsoleMessage promotes every JavaScript-source/error-level console message to a page error: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/webkit/wkPage.ts#L538

Expected and suggested approach

Keep the native failed-send diagnostic observable as a console/transport error, distinguishable from an unhandled JavaScript exception. Consider carrying an exception/native diagnostic discriminator in the WebKit protocol and using it in _onConsoleMessage, rather than promoting every JavaScript-source error. A message-text exclusion would risk concealing actual exceptions with matching text. I have not built or tested an upstream protocol patch.

Workaround and application limits

In our explicit departure test, wait for both surviving peers to process broker departure and dispose the departed connection before sending a new remaining-pair record. This avoids deliberately racing a stale peer and preserves the unchanged readiness deadline and pageerror checks. Do not ignore all RTC errors or infer remote delivery from send() returning; durable reconnect reconciliation/acknowledgements are still necessary.

Related prior WebKit native-console classification report: https://github.com/microsoft/playwright/issues/43120
Investigation and exact failed hosted run: https://github.com/link-foundation/meta-sovereign/pull/57 and https://github.com/link-foundation/meta-sovereign/actions/runs/43462136209/job/132013458396 .

主要语言
TypeScript
星标
99.7k
派生
6.8k
平均合并
16 小时 10 分钟
30 天内合并 PR
301

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

microsoft/playwright 的其他 Issue

查看 microsoft/playwright 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。