Skip to content

Browser Run Integration

Browser Run Integration

This document describes the Cloudflare Browser Run integration in bloqr-backend. Browser Run allows the worker to spin up a full Chromium instance to fetch filter-list sources that require JavaScript execution, navigate redirect chains, or run behind bot-detection walls that defeat a plain fetch().

What changed? Cloudflare rebranded “Browser Rendering” to Browser Run and rebuilt the service on top of Cloudflare Containers. The [browser] binding API and @cloudflare/playwright package are unchanged — no code changes are required.


Binding Configuration

Add the BROWSER binding to wrangler.toml:

[browser]
binding = "BROWSER"

The binding type is Fetcher (Cloudflare’s internal cloudflare:workers WorkerEntrypoint type). The @cloudflare/playwright package wraps it into a Playwright-compatible API.


Architecture

Single Source of Truth: BrowserFetcher

All Playwright structural interfaces and the EXTRACT_TEXT_SCRIPT constant live exclusively in src/platform/BrowserFetcher.ts. Nothing is duplicated in the worker layer.

mindmap
  root((src/platform/BrowserFetcher.ts))
    browserWorker["IBrowserWorker — type of env.BROWSER"]
    playwrightBrowser["IPlaywrightBrowser — slim Playwright Browser interface"]
    playwrightPage["IPlaywrightPage — slim Playwright Page interface"]
    browserConnector["BrowserConnector — (binding: IBrowserWorker) => Promise<IPlaywrightBrowser>"]
    options["BrowserFetcherOptions — timeout, waitUntil, returnHtml"]
    extractScript["EXTRACT_TEXT_SCRIPT — inline script injected into the page to extract filter rules"]
    browserFetcher["BrowserFetcher — IContentFetcher implementation"]

Worker utilities in worker/handlers/browser.ts import types and the constant from BrowserFetcher.ts. They never re-define them.

Dynamic Import

@cloudflare/playwright imports cloudflare:workers at module level. Loading it statically crashes Deno’s test runner. Worker code therefore loads it lazily:

worker/handlers/browser.ts
const { launch } = await import('@cloudflare/playwright');
const browser = await launch(binding);

This keeps the module out of Deno’s static graph during deno task check and deno task test.


Worker Endpoints

All three endpoints require the caller to be authenticated with a Bearer token.

POST /api/browser/resolve-url

Navigates to a URL and returns the final canonical URL after all redirects.

Request body

{
"url": "https://example.com/short-link", // required
"waitUntil": "networkidle" // optional, see §waitUntil Options
}

Response 200

{
"success": true,
"resolvedUrl": "https://example.com/final-destination",
"originalUrl": "https://example.com/short-link"
}

Use case: Discover the true destination of a redirect chain before scheduling a filter-list download. Saves bandwidth on subsequent compilation runs.


POST /api/browser/monitor

Performs parallel browser-based health checks on a list of filter-list source URLs. For each URL the handler navigates with a headless browser, verifies non-empty text content, and optionally captures a full-page PNG screenshot stored in R2.

The full result set is persisted to the KV key browser:monitor:latest so it can be retrieved later without re-running the checks.

Request body

{
"urls": [ // required, 1–10
"https://example.com/filter-list.txt",
"https://another.example.com/rules.txt"
],
"captureScreenshots": false, // optional — store a PNG per URL in R2
"screenshotPrefix": "2025-07-01", // optional — R2 key prefix (default: ISO date)
"timeout": 30000, // optional — per-URL timeout in ms
"waitUntil": "networkidle" // optional, see §waitUntil Options
}

Response 200

{
"success": true,
"total": 2,
"reachable": 1,
"unreachable": 1,
"results": [
{
"url": "https://example.com/filter-list.txt",
"reachable": true,
"checkedAt": "2025-07-01T12:00:00.000Z",
"screenshotKey": "2025-07-01/a1b2c3d4e5f6.png"
},
{
"url": "https://another.example.com/rules.txt",
"reachable": false,
"error": "net::ERR_NAME_NOT_RESOLVED",
"checkedAt": "2025-07-01T12:00:01.000Z"
}
]
}

screenshotKey is only present when captureScreenshots is true and FILTER_STORAGE is configured. When a URL cannot be fetched, the result entry contains an error field and reachable is false.

Required bindings: BROWSER Optional bindings: FILTER_STORAGE (R2 for screenshots), COMPILATION_CACHE (KV for persistence)


GET /api/browser/monitor/latest

Returns the most recent result set written by POST /api/browser/monitor. Useful for polling or dashboards that need to display change status without triggering new browser navigations.

Response 200 — same shape as POST /api/browser/monitor

Response 404 — no monitor run has been persisted yet

Required binding: COMPILATION_CACHE


waitUntil Options

All browser endpoints accept an optional waitUntil field that controls when Playwright considers a page navigation complete.

ValueDescription
loadFires when the load DOM event fires. Fastest; suitable for static pages.
domcontentloadedFires when the DOMContentLoaded event fires.
networkidle(default) Waits until no network connections for 500 ms. Best for SPA-heavy pages.

ISource.useBrowser Flag

Set useBrowser: true on any source in a WorkerCompiler configuration to route that source’s download through BrowserFetcher instead of the standard HTTP fetcher:

import { WorkerCompiler } from '@jk-com/bloqr-compiler';
const compiler = new WorkerCompiler({
fetcher: httpFetcher,
browserConnector: launch, // from @cloudflare/playwright
browserBinding: env.BROWSER,
});
const result = await compiler.compile({
sources: [
{
source: 'https://example.com/plain-list.txt',
},
{
source: 'https://js-heavy-site.example.com/rules',
useBrowser: true, // ← uses BrowserFetcher for this source only
},
],
// ...
});

When useBrowser is true but browserConnector or browserBinding are not provided to WorkerCompiler, the compiler throws an error.


Using BrowserFetcher Directly

BrowserFetcher is exported from the library and can be used outside the Worker:

import { BrowserFetcher } from '@jk-com/bloqr-compiler';
// In a Cloudflare Worker:
import { launch } from '@cloudflare/playwright';
const fetcher = new BrowserFetcher(
env.BROWSER,
{ timeout: 30_000, waitUntil: 'networkidle' },
launch,
);
const content = await fetcher.fetch('https://example.com/filter-list.txt');

Required Bindings Summary

BindingTypeRequired for
BROWSERFetcherAll browser navigation
FILTER_STORAGER2BucketScreenshot capture (POST /api/browser/monitor with captureScreenshots: true)
COMPILATION_CACHEKVNamespaceResult persistence (POST /api/browser/monitor, GET /api/browser/monitor/latest)

Both BROWSER and COMPILATION_CACHE are already declared in worker/types.ts (Env interface) and wrangler.toml.