Skip to navigation

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.

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

Installation

npm i patchright

Python:

pip install patchright

Basic connection

import { chromium } from "patchright";
import { writeFileSync } from "fs";
// 1. Create a session
const { cdp_url, id } = await fetch(
"https://api.chaser.sh/v1/sessions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CHASER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ttl_seconds: 1800 }),
}
).then((r) => r.json());
// 2. Connect
const browser = await chromium.connectOverCDP(cdp_url);
const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();
// 3. Automate
await page.goto("https://www.cloudflare.com");
console.log(await page.title());
// 4. Screenshot via the API (not page.screenshot())
const png = await fetch(
`https://api.chaser.sh/v1/sessions/${id}/screenshot`,
{
method: "POST",
headers: { Authorization: `Bearer ${process.env.CHASER_KEY}` },
}
).then((r) => r.arrayBuffer());
writeFileSync("page.png", Buffer.from(png));
// 5. Clean up
await fetch(`https://api.chaser.sh/v1/sessions/${id}`, { method: "DELETE" });

Python

import os, requests
from patchright.sync_api import sync_playwright
# 1. Create a session
session = requests.post(
"https://api.chaser.sh/v1/sessions",
headers={"Authorization": f"Bearer {os.environ['CHASER_KEY']}"},
json={"ttl_seconds": 1800},
).json()
# 2. Connect
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session["cdp_url"])
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://www.cloudflare.com")
print(page.title())
# 3. Screenshot
png = requests.post(
f"https://api.chaser.sh/v1/sessions/{session['id']}/screenshot",
headers={"Authorization": f"Bearer {os.environ['CHASER_KEY']}"},
).content
with open("page.png", "wb") as f:
f.write(png)
# 4. Clean up
requests.delete(
f"https://api.chaser.sh/v1/sessions/{session['id']}",
headers={"Authorization": f"Bearer {os.environ['CHASER_KEY']}"},
)

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":

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

Handling disconnection

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

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

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