How to Handle and Expect File Downloads with Zendriver

Zendriver provides a built-in async API for intercepting file downloads via the Chrome DevTools Protocol, using Tab.expect_download() as an async context manager that captures the Browser.downloadWillBegin event while temporarily blocking automatic downloads.

Zendriver is a Python library that wraps the Chrome DevTools Protocol (CDP) to provide high-level browser automation. When you need to handle and expect file downloads with Zendriver, the library offers a dedicated expectation system that intercepts download events before they complete, giving you full control over file naming, storage locations, and data processing.

Configure the Download Directory (Optional)

By default, Zendriver automatically creates a downloads directory in your current working directory and enables downloads by calling Browser.setDownloadBehavior(behavior="allow"). However, you can specify a custom path using Tab.set_download_path().

According to the source code in zendriver/core/tab.py (lines 1413-1428), this method sends the CDP command Browser.setDownloadBehavior with behavior="allow" and your resolved path, storing the tuple ["allow", <path>] on the tab instance for later reuse:

import zendriver as zd

async with await zd.start() as browser:
    page = browser.main_tab
    await page.set_download_path("/tmp/my_downloads")  # Custom directory

    
    # Downloads will now save to /tmp/my_downloads automatically

    await page.get("https://example.com/download-page")

Intercept Downloads with expect_download()

The primary mechanism for handling file downloads is the expect_download() method available on both Tab and Page objects. This returns a DownloadExpectation instance that acts as an async context manager, temporarily modifying browser behavior to intercept the next download event.

According to the implementation in zendriver/core/expect.py (lines 88-130), entering the context manager performs three critical actions:

  1. Blocks automatic downloads by sending Browser.setDownloadBehavior(behavior="deny", events_enabled=True)
  2. Registers a temporary CDP handler for the Browser.downloadWillBegin event
  3. Creates an internal future that resolves when the download begins

When the context exits, the library automatically restores the previous download behavior—either the default configuration or the custom path you set via set_download_path().

Triggering and Capturing the Download

Inside the context manager, perform any UI action that initiates a download, such as clicking a button or submitting a form. The expectation waits asynchronously for the downloadWillBegin event and exposes the captured data through its value property:

async with page.expect_download() as dl:
    # Trigger the download via UI interaction

    await (await page.select("#downloadButton")).mouse_click()
    
    # Wait for the event and retrieve the download object

    download = await dl.value

Accessing Download Metadata

The resolved download object is a cdp.browser.DownloadWillBegin instance containing:

  • suggested_filename – The filename Chrome would normally use for the file
  • url – Either a standard URL or a base64-encoded data URL (data:application/octet-stream;base64,...)
  • guid – Unique identifier assigned to the download
  • totalBytes – Expected total size of the file in bytes

Processing Download Data

When expect_download() captures an event, you have two options for handling the actual file content, depending on how you configured the download path.

If you set a custom download directory, Chrome writes the file directly to disk while the event object provides the metadata. If the url field contains a data URL (common with JavaScript-generated downloads), you must manually decode the base64 content:

import os
import base64

async with page.expect_download() as dl:
    await (await page.select("#exportBtn")).click()
    download = await dl.value

# Handle data URL format

if download.url.startswith("data:"):
    path = os.path.join("./downloads", download.suggested_filename)
    with open(path, "wb") as f:
        # Extract base64 content after the comma

        data = base64.b64decode(download.url.split(",", 1)[-1])
        f.write(data)

Direct File Downloads with download_file()

For programmatic downloads without UI interaction, use the high-level Tab.download_file() helper. Located in zendriver/core/tab.py (lines 1235-1280), this method injects a JavaScript snippet that creates a temporary <a> element with the download attribute and programmatically clicks it. This approach works for cross-origin resources because the fetch occurs within the page context:

async with await zd.start() as browser:
    tab = browser.main_tab
    await tab.set_download_path("./downloads")
    
    # Download directly from URL

    await tab.download_file(
        "https://example.com/files/report.pdf",
        filename="quarterly_report.pdf"
    )
    # File appears at ./downloads/quarterly_report.pdf

Complete Working Examples

Basic Download Expectation Pattern

The official example in examples/expect_download.py demonstrates the full workflow with a data URL download:

import asyncio
import base64
import os
import zendriver as zd

async def main() -> None:
    out_dir = "."
    async with await zd.start() as browser:
        page = browser.main_tab
        await page.get("https://translate.yandex.com/en/ocr")
        
        async with page.expect_download() as dl:
            await (await page.select("#downloadButton")).mouse_click()
            download = await dl.value

        print("Filename:", download.suggested_filename)
        
        # Save from data URL

        path = os.path.join(out_dir, download.suggested_filename)
        with open(path, "wb") as f:
            data = base64.b64decode(download.url.split(",", 1)[-1])
            f.write(data)

if __name__ == "__main__":
    asyncio.run(main())

Testing with pytest

The test suite in tests/core/test_tab.py (lines 250-259) shows how to verify downloads in automated tests:

import pytest
import asyncio
import zendriver as zd

@pytest.mark.asyncio
async def test_expect_download(browser: zd.Browser):
    page = browser.main_tab
    await page.get("https://my.site/has_download")
    
    async with page.expect_download() as dl:
        await (await page.select("#downloadBtn")).click()
        download = await asyncio.wait_for(dl.value, timeout=5)

    assert isinstance(download, zd.cdp.browser.DownloadWillBegin)
    assert download.suggested_filename.endswith(".zip")

Summary

  • Zendriver handles file downloads through CDP events rather than simple HTTP interception, providing access to browser-level download metadata.
  • Use Tab.set_download_path() (defined in zendriver/core/tab.py) to configure persistent download directories, or rely on the default ./downloads folder.
  • Wrap download-triggering actions in Tab.expect_download() (implemented in zendriver/core/expect.py) to capture the Browser.downloadWillBegin event and prevent automatic downloading while inside the context.
  • The DownloadWillBegin object provides suggested_filename, url, and guid fields—process data URLs manually with base64 decoding when necessary.
  • For server-initiated downloads without UI, use Tab.download_file() to inject JavaScript download triggers that bypass cross-origin restrictions.

Frequently Asked Questions

Does Zendriver automatically save files to disk?

By default, yes. If you do not call set_download_path() manually, Zendriver automatically creates a downloads directory and enables automatic saving via Browser.setDownloadBehavior(behavior="allow"). However, when using expect_download(), the context manager temporarily sets behavior to "deny" to intercept the event, requiring you to either process the data URL manually or rely on the pre-configured download directory.

How do I handle cross-origin downloads in Zendriver?

For cross-origin resources that trigger browser download dialogs, use Tab.download_file() as implemented in zendriver/core/tab.py (lines 1235-1280). This method executes JavaScript inside the page context using Runtime.callFunctionOn, creating a temporary anchor element with the download attribute. Because the fetch occurs within the page's execution context, it bypasses cross-origin restrictions that would block direct HTTP requests.

Can I rename files during the download process?

Yes. When using Tab.download_file(), pass your desired filename as the second argument. For downloads captured via expect_download(), the suggested_filename property reflects Chrome's default naming, but you control the final filesystem name when writing the file—either by specifying the destination path when saving data URL content, or by renaming the file after Chrome writes it to your configured download directory.

What CDP events does Zendriver use for downloads?

Zendriver primarily uses the Browser.downloadWillBegin event, defined in zendriver/cdp/browser.py. When expect_download() is active, the library registers a temporary handler for this event while setting Browser.setDownloadBehavior(behavior="deny", events_enabled=True) to ensure the event fires without the browser automatically saving the file. The DownloadExpectation class manages this lifecycle, restoring previous behavior when the context manager exits.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →