Getting Started
CDP is available globally via the cdp table. While it works in any hook, it is most commonly used in override_fetch to replace the default HTTP client.
Basic Usage
Section titled “Basic Usage”function override_fetch(request, ctx) local browser = cdp.launch({}) defer(function() browser:close() end)
local page = browser:attach() local ok, err = page:open(request.url) if not ok then return { error = "Navigation failed: " .. tostring(err) } end
local found, wait_err = page:wait_for_selector(".dynamic-content", 10000) if not found then return { error = "Selector timeout: " .. tostring(wait_err) } end
return { status = 200, body = page:content() }endname = "JS Rendering"url = "https://example.com"selector = ".item"fields = ["title:h2"]interval = 300Persistent Browser Pattern
Section titled “Persistent Browser Pattern”Launching a new browser for every fetch is slow. Store the browser in a global variable to keep it alive between iterations:
if not browser then browser = cdp.launch({ headless = true, keep_alive = true })end
function override_fetch(request, ctx) local page = browser:attach() local ok, err = page:open(request.url) if not ok then page:close() return { error = "Failed to load " .. request.url } end
if page:wait_for_selector(".item", 5000) then local html = page:content() page:close() return { status = 200, body = html, url = request.url } else page:close() return { error = "Content timeout" } endendAsync vs Sync
Section titled “Async vs Sync”Methods are marked as (Async) or (Sync). The difference is purely technical and usually only matters inside defer:
- Async frees the thread to run other tasks while waiting for I/O. Your hook appears to block but the runtime stays responsive.
- Sync holds the thread until the binding finishes; only used for instant operations like computation or simple reads.