Skip to content

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.

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() }
end

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" }
end
end

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.