Playwright / Patchright

View as Markdown

Connect to Chaser sessions with patchright, a drop-in Playwright replacement that patches detectable automation artifacts in Chromium.

Why patchright?

The Chromium bundled with stock Playwright ships with properties that anti-bot systems check:

  • navigator.webdriver === true
  • Missing chrome.runtime
  • window.chrome inconsistencies
  • Headless-specific user-agent tokens

Patchright patches these at the browser level. The API is identical to Playwright — same imports, same types, same methods. Swap the import and the session looks like an unmodified user browser.

1- import { chromium } from "playwright";
2+ import { chromium } from "patchright";

Installation

$npm i patchright

Python:

$pip install patchright

Basic connection

1import { chromium } from "patchright";
2import { writeFileSync } from "fs";
3
4// 1. Create a session
5const { cdp_url, id } = await fetch(
6 "https://api.chaser.sh/v1/sessions",
7 {
8 method: "POST",
9 headers: {
10 Authorization: `Bearer ${process.env.CHASER_KEY}`,
11 "Content-Type": "application/json",
12 },
13 body: JSON.stringify({ ttl_seconds: 1800 }),
14 }
15).then((r) => r.json());
16
17// 2. Connect
18const browser = await chromium.connectOverCDP(cdp_url);
19const context = browser.contexts()[0];
20const page = context.pages()[0] ?? await context.newPage();
21
22// 3. Automate
23await page.goto("https://www.cloudflare.com");
24console.log(await page.title());
25
26// 4. Screenshot via the API (not page.screenshot())
27const png = await fetch(
28 `https://api.chaser.sh/v1/sessions/${id}/screenshot`,
29 {
30 method: "POST",
31 headers: { Authorization: `Bearer ${process.env.CHASER_KEY}` },
32 }
33).then((r) => r.arrayBuffer());
34writeFileSync("page.png", Buffer.from(png));
35
36// 5. Clean up
37await fetch(`https://api.chaser.sh/v1/sessions/${id}`, { method: "DELETE" });

Python

1import os, requests
2from patchright.sync_api import sync_playwright
3
4# 1. Create a session
5session = requests.post(
6 "https://api.chaser.sh/v1/sessions",
7 headers={"Authorization": f"Bearer {os.environ['CHASER_KEY']}"},
8 json={"ttl_seconds": 1800},
9).json()
10
11# 2. Connect
12with sync_playwright() as p:
13 browser = p.chromium.connect_over_cdp(session["cdp_url"])
14 context = browser.contexts[0]
15 page = context.pages[0] if context.pages else context.new_page()
16 page.goto("https://www.cloudflare.com")
17 print(page.title())
18
19# 3. Screenshot
20png = requests.post(
21 f"https://api.chaser.sh/v1/sessions/{session['id']}/screenshot",
22 headers={"Authorization": f"Bearer {os.environ['CHASER_KEY']}"},
23).content
24with open("page.png", "wb") as f:
25 f.write(png)
26
27# 4. Clean up
28requests.delete(
29 f"https://api.chaser.sh/v1/sessions/{session['id']}",
30 headers={"Authorization": f"Bearer {os.environ['CHASER_KEY']}"},
31)

Waiting for the session to be ready

POST /v1/sessions returns immediately with status: "creating". The cdp_url is present in the response but the session may not be ready yet. Poll until status is "ready":

1async function waitForSession(sessionId: string): Promise<void> {
2 while (true) {
3 const s = await fetch(
4 `https://api.chaser.sh/v1/sessions/${sessionId}`,
5 { headers: { Authorization: `Bearer ${process.env.CHASER_KEY}` } }
6 ).then((r) => r.json());
7
8 if (s.status === "ready") return;
9 if (s.status === "error") throw new Error("Session failed");
10 await new Promise((r) => setTimeout(r, 2000));
11 }
12}

Handling disconnection

When the session expires or is deleted, the WebSocket closes. Listen for the disconnected event:

1browser.on("disconnected", () => {
2 console.log("Session ended");
3});

What not to do

  • Don’t call chromium.launch() — use connectOverCDP
  • Don’t call browser.newContext() — use browser.contexts()[0]
  • Don’t use page.screenshot() — use the /screenshot endpoint
  • Don’t override the user-agent or viewport — it breaks fingerprint consistency