Skip to content

[Bug]: Cross-Platform CDP Download Path Issue - Linux Client → Windows CDP Server #38805

Description

Bug Description

When using chromium.connectOverCDP() from a Linux client to connect to a Windows CDP server, file downloads fail because Playwright sends Linux paths (/tmp/playwright-artifacts-XXXXXX) to Windows Chrome, which cannot write to these paths.

Environment

  • Playwright Version: 1.57.0 (latest)
  • Client OS: Linux (Ubuntu 22.04, Alpine, etc.)
  • Server OS: Windows 10/11
  • Browser: Chromium-based (Chrome, Edge)
  • Connection Method: chromium.connectOverCDP()

Reproduction

Minimal Test Case

const playwright = require('playwright-core');

async function reproduce() {
  // Connect to Windows CDP server
  const browser = await playwright.chromium.connectOverCDP('ws://windows-host:9222/devtools/browser/...');
  
  // Create context with acceptDownloads
  const context = await browser.newContext({ acceptDownloads: true });
  const page = await context.newPage();
  
  // Trigger download
  const downloadPromise = page.waitForEvent('download');
  await page.goto('https://example.com/file.pdf');
  const download = await downloadPromise;
  
  // This will fail with ENOENT
  await download.saveAs('./local-file.pdf');
  
  await browser.close();
}

reproduce();

Expected Behavior

File should be downloaded and saved to ./local-file.pdf on the Linux client.

Actual Behavior

Error: download.saveAs: ENOENT: no such file or directory, 
copyfile '/tmp/playwright-artifacts-MedLPD/537add71-ff6a-4dfa-85e7-f6f2f68080f8'

Root Cause Analysis

1. Path Generation on Linux Client

In packages/playwright-core/src/server/chromium/chromium.ts:

// Line 122-124
const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER));
const browserOptions: BrowserOptions = {
  // ...
  downloadsPath: options.downloadsPath || artifactsDir,  // ← Linux path!
  // ...
};

The artifactsDir is created on the Linux client using fs.promises.mkdtemp().

2. CDP Command Sent to Windows

In packages/playwright-core/src/server/chromium/crBrowser.ts:

// Line 351-358
promises.push(this._browser._session.send('Browser.setDownloadBehavior', {
  behavior: this._options.acceptDownloads === 'accept' ? 'allowAndName' : 'deny',
  browserContextId: this._browserContextId,
  downloadPath: this._browser.options.downloadsPath,  // ← Linux path sent to Windows!
  eventsEnabled: true,
}));

This Linux path (/tmp/playwright-artifacts-XXXXXX) is sent to Windows Chrome via CDP.

3. Windows Chrome Cannot Write

Windows Chrome receives the Linux path and attempts to write to it, resulting in:

  • Path doesn't exist
  • Permission errors
  • ENOENT error

4. saveAs() Fails

In packages/playwright-core/src/client/artifact.ts:

// Line 45-55
async saveAs(path: string): Promise<void> {
  if (!this._connection.isRemote()) {
    await this._channel.saveAs({ path });
    return;
  }

  // Remote connection - tries to copy from local path
  const result = await this._channel.saveAsStream();
  // ... but the file is on Windows, not Linux!
}

The saveAs() method tries to copy from a local Linux path that doesn't exist.

Impact

Affected Users

  1. Cross-platform testing teams (Linux CI → Windows browser)
  2. Docker users (Linux container → Windows host browser)
  3. Remote browser services (Browserbase, Browserless, etc.)
  4. Enterprise testing (Linux servers → Windows test machines)

Workarounds

  1. Network Interception (Complex, manual implementation)
  2. File Server (Requires additional server on Windows)
  3. Direct HTTP Download (Not always possible)
  4. Switch to Puppeteer (Better CDP support for large files)

Proposed Solutions

Solution 1: Path Translation (Recommended)

Add automatic path translation for cross-platform CDP connections:

// In chromium.ts connectOverCDPInternal
if (options.isLocal === false) {
  // Detect target OS and use appropriate path
  const targetOS = detectTargetOS(wsEndpoint);
  if (targetOS === 'windows') {
    downloadsPath: 'C:\\Users\\Administrator\\Downloads\\playwright-downloads';
  } else if (targetOS === 'linux') {
    downloadsPath: '/tmp/playwright-downloads';
  }
}

Solution 2: Document the Limitation

Add clear warning in official documentation:

Warning: When using connectOverCDP() to connect to a remote browser on a different operating system, file downloads may fail due to path translation issues. Consider using:

  • Network interception with IO.read
  • File server approach
  • Direct HTTP downloads

Solution 3: Provide Official Workaround

Create a helper function or middleware:

// New API
const browser = await chromium.connectOverCDP({
  endpointURL: 'ws://windows-host:9222/devtools/browser/...',
  downloadsPath: 'C:\\Users\\Administrator\\Downloads\\playwright-downloads'  // Windows path
});

Related Issues

Additional Context

Playwright's Official Stance

From issue #34542 (closed as "not planned"):

"This is expected behavior. When connecting over CDP, Playwright cannot access the remote browser's filesystem."

Counter-argument: Playwright CAN access remote files via:

  • Network interception + IO.read (CDP command)
  • File server approach
  • Direct HTTP downloads

The limitation is implementation choice, not technical impossibility.

Comparison with Puppeteer

Puppeteer handles large file transfers (>50MB) over CDP without issues, while Playwright has a 50MB hard limit.

Suggested Labels

  • bug
  • cross-platform
  • CDP
  • download
  • connectOverCDP
  • help-wanted

Suggested Priority

Medium - Affects a significant subset of users doing cross-platform testing, but has workarounds.

Additional Files

  • /root/Documents/playwright_cdp_download.md - Complete research on CDP download behavior
  • /root/Documents/playwright_cdp_download研究报告.md - Technical analysis in Chinese
  • /root/Documents/bug_analysis.md - Detailed bug analysis

Conclusion

This is a design flaw in Playwright's cross-platform CDP implementation. The issue is well-documented in multiple GitHub issues, but remains unresolved. A fix would significantly improve the cross-platform testing experience for many users.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions