Skip to content

Downloads triggered by window.open() never reach page.expect_download() #194

Description

@adhami3310

Summary

A download triggered by window.open() never reaches page.expect_download(). Playwright attributes such a download to the opener page and resolves normally; rustwright waits out the timeout.

Downloads triggered by a same-tab navigation (location.href = url), by an <a download> element, or by a blob URL all work correctly — only the window.open() path is affected.

Repro

Deterministic, local, no network. The server responds to /file.txt with Content-Disposition: attachment, so the popup never becomes a page — it becomes a download.

import asyncio, http.server, socket, sys, threading

PAGE = b"""<!doctype html><html><body>opener</body></html>"""

class H(http.server.BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    def do_GET(self):
        if self.path.startswith("/file.txt"):
            body = b"file contents"
            self.send_response(200)
            self.send_header("Content-Type", "application/octet-stream")
            self.send_header("Content-Disposition", 'attachment; filename="hello.txt"')
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        self.send_response(200)
        self.send_header("Content-Type", "text/html")
        self.send_header("Content-Length", str(len(PAGE)))
        self.end_headers()
        self.wfile.write(PAGE)
    def log_message(self, *a): pass

def serve():
    s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close()
    srv = http.server.ThreadingHTTPServer(("127.0.0.1", p), H); srv.daemon_threads = True
    threading.Thread(target=srv.serve_forever, daemon=True).start()
    return p

async def run(module, port):
    api = __import__(f"{module}.async_api", fromlist=["async_playwright"])
    async with api.async_playwright() as pw:
        b = await pw.chromium.launch(headless=True)
        for label, js in (
            ("window.open(url, '_blank')", "() => window.open('/file.txt', '_blank')"),
            ("window.open(url)",           "() => window.open('/file.txt')"),
            ("location.href",              "() => { window.location.href = '/file.txt'; }"),
        ):
            page = await b.new_page(accept_downloads=True)
            await page.goto(f"http://127.0.0.1:{port}/")
            try:
                async with page.expect_download(timeout=8000) as info:
                    await page.evaluate(js)
                dl = await info.value
                print(f"    {module:11s} {label:28s}: OK  {dl.suggested_filename!r}")
            except Exception as exc:
                print(f"    {module:11s} {label:28s}: {type(exc).__name__}: {str(exc)[:60]}")
            await page.close()
        await b.close()

PORT = serve()
for m in sys.argv[1:] or ["playwright", "rustwright"]:
    asyncio.run(run(m, PORT))

Result

    playwright  window.open(url, '_blank')  : OK  'hello.txt'
    playwright  window.open(url)            : OK  'hello.txt'
    playwright  location.href               : OK  'hello.txt'
    rustwright  window.open(url, '_blank')  : TimeoutError: Timeout 8000ms exceeded while waiting for event "download"
    rustwright  window.open(url)            : TimeoutError: Timeout 8000ms exceeded while waiting for event "download"
    rustwright  location.href               : OK  'hello.txt'

Expected: the window.open rows resolve like the location.href row.

Environment

  • rustwright 0.1.1 (PyPI) and git ec130a64 — both reproduce
  • playwright 1.61.0 for comparison
  • Chromium headless shell 1228, Linux x86_64, Python 3.14

Why it matters

Found while evaluating rustwright as a drop-in for a Playwright suite. The app under test downloads a file by redirecting the browser to a presigned URL through window.open, which is a common pattern for "download this artifact" flows where the URL is generated server-side.

The failure is hard to attribute from the test side: the click lands, the server receives and serves the request (confirmed in the application's own logs), and only the expect_download wait times out — so it reads as a broken download feature rather than a missing event.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions