# How to Handle and Expect File Downloads with Zendriver

> Learn how to handle and expect file downloads with Zendriver. Use the async API and Tab.expect_download() to intercept and capture browser downloads effectively.

- Repository: [CDP Driver/zendriver](https://github.com/cdpdriver/zendriver)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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`](https://github.com/cdpdriver/zendriver/blob/main/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:

```python
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`](https://github.com/cdpdriver/zendriver/blob/main/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:

```python
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:

```python
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`](https://github.com/cdpdriver/zendriver/blob/main/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:

```python
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`](https://github.com/cdpdriver/zendriver/blob/main/examples/expect_download.py) demonstrates the full workflow with a data URL download:

```python
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`](https://github.com/cdpdriver/zendriver/blob/main/tests/core/test_tab.py) (lines 250-259) shows how to verify downloads in automated tests:

```python
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`](https://github.com/cdpdriver/zendriver/blob/main/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`](https://github.com/cdpdriver/zendriver/blob/main/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`](https://github.com/cdpdriver/zendriver/blob/main/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`](https://github.com/cdpdriver/zendriver/blob/main/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.