← Public packages
@kody/browser-run
Call Cloudflare Browser Run Quick Actions and reuse shared sessions with createBrowserContext.
src/worker-recipe.ts
128 lines · 4.5 KB · TypeScriptimport { object, optional, parse, string } from 'remix/data-schema'
import { DOCS_REUSE_SESSIONS } from './client.ts'
const recipeInput = object(
{
bindingName: optional(string()),
compatibilityDate: optional(string()),
},
{ unknownKeys: 'error' },
)
/**
* Documentation-as-data for a Cloudflare Worker that uses a browser binding
* plus `@cloudflare/puppeteer` ≥ 1.1.0: list sessions → connect →
* createBrowserContext → disconnect (never browser.close on shared sessions).
* Not runnable Puppeteer inside Kody — copy into a Worker you deploy.
*
* @param raw.bindingName - Wrangler browser binding name (default MYBROWSER)
* @param raw.compatibilityDate - Must be ≥ 2025-09-15 for concurrent clients
* @returns Structured recipe: wrangler snippet, Worker TypeScript, footguns
*
* @example
* import workerRecipe from 'kody:@kody/browser-run/worker-recipe'
* const recipe = await workerRecipe({ bindingName: 'MYBROWSER' })
*/
export default async function workerRecipe(raw: unknown = {}) {
const input = parse(recipeInput, raw ?? {})
const binding = (input.bindingName || 'MYBROWSER').trim() || 'MYBROWSER'
const compatibilityDate = (input.compatibilityDate || '2026-09-29').trim()
return {
purpose:
'Deploy a Worker that reuses Browser Run sessions with per-request createBrowserContext isolation.',
docs: DOCS_REUSE_SESSIONS,
requirements: {
puppeteer: '@cloudflare/puppeteer >= 1.1.0',
playwright: '@cloudflare/playwright >= 1.3.0 (if using Playwright)',
compatibilityFlags: ['nodejs_compat'],
compatibilityDateMin: '2025-09-15',
compatibilityDate,
browserBinding: binding,
},
wranglerJsonc: {
name: 'browser-worker',
main: 'src/index.ts',
compatibility_date: compatibilityDate,
compatibility_flags: ['nodejs_compat'],
browser: { binding },
},
install: 'npm i -D @cloudflare/puppeteer',
workerTypeScript: `
import puppeteer from "@cloudflare/puppeteer";
const MAX_CONCURRENT_CONTEXTS = 4;
interface Env {
${binding}: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
let reqUrl = url.searchParams.get("url") || "https://example.com";
reqUrl = new URL(reqUrl).toString();
const sessions = await puppeteer.sessions(env.${binding});
const start = sessions.length ? Math.floor(Math.random() * sessions.length) : 0;
const ordered = sessions.length
? [...sessions.slice(start), ...sessions.slice(0, start)]
: [];
let browser;
let launched = false;
for (const session of ordered) {
try {
const candidate = await puppeteer.connect(env.${binding}, session.sessionId);
try {
const client = await candidate.target().createCDPSession();
try {
const { browserContextIds } = await client.send("Target.getBrowserContexts");
if (browserContextIds.length < MAX_CONCURRENT_CONTEXTS) {
browser = candidate;
break;
}
} finally {
await client.detach();
}
} finally {
if (candidate !== browser) await candidate.disconnect();
}
} catch {
// Session may have closed after it was listed
}
}
if (!browser) {
browser = await puppeteer.launch(env.${binding});
launched = true;
}
const sessionId = browser.sessionId();
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
const response = await page.goto(reqUrl);
const html = await response!.text();
return new Response(
(launched ? "Launched " : "Connected to ") + sessionId + "\\n-----\\n" + html,
{ headers: { "content-type": "text/plain" } },
);
} finally {
await context.close();
await browser.disconnect(); // NEVER browser.close() while sharing
}
},
};
`.trim(),
footguns: [
'Never call browser.close() when other clients may share the session.',
'Always createBrowserContext() per request, then context.close().',
'Use browser.disconnect() to drop your CDP connection only.',
'Older @cloudflare/puppeteer (<1.1.0) is single-connection — upgrade.',
'compatibility_date must be 2025-09-15 or later with nodejs_compat.',
'For local wrangler dev with a real browser, set browser.remote: true.',
],
kodyNote:
'This package does not run Puppeteer inside Kody isolates. Use ./open-isolated + ./sessions/* for REST/CDP metadata, or deploy this Worker recipe on Cloudflare.',
}
}