Skyvern-AI / Skyvern-AI/rustwright

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

Open
#194 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
890
Forks
58
Avg merge
20h 29m
Merged PRs (30d)
12

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the async_api page.expect_download flow and trace how downloads from window.open are associated with the opener page. Compare that path with the working location.href case in the provided deterministic reproduction. Done means both window.open variants resolve page.expect_download with the expected hello.txt filename, while existing cases continue to work.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, rust
Domain
devtools, testing-qa
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.