Commit Graph

860 Commits

Author SHA1 Message Date
Adrià Arrufat
3a01114745 CDP: evict only replaced page nodes on re-goto
Introduce `Registry.resetFrame` to selectively evict nodes owned by
the replaced page's frame. This prevents invalidating node IDs of
sibling pages during concurrent navigations.
2026-06-30 09:39:46 +02:00
Karl Seguin
459daecbea zig fmt 2026-06-30 13:05:51 +08:00
Karl Seguin
c754c999a4 minor: silence log in test that intentionally generates an error 2026-06-30 13:04:24 +08:00
Pierre Tachoire
cf99ec2fbd Merge pull request #2826 from bebsworthy/fix/cdp-frameattached-double-params
cdp: Fix Page.frameAttached params double-wrapping that broke sub-frame interception
2026-06-29 07:50:25 +00:00
Karl Seguin
01ad869dff Merge pull request #2831 from lightpanda-io/unused_imports
minor: remove unused imports
2026-06-28 07:46:43 +08:00
Halil Durak
a457ddf8a7 Merge pull request #2703 from lightpanda-io/nikneym/rework-url-resolve
`URL`: Rework `resolve` with new URL implementation
2026-06-27 23:56:47 +03:00
Karl Seguin
0aaa77c7e6 minor: remove unused imports 2026-06-27 18:16:31 +08:00
Boyd Ebsworthy
e6bd65626e cdp: Fix Page.frameAttached params double-wrapping that broke sub-frame interception
frameChildFrameCreated emitted Page.frameAttached with its payload wrapped
in an extra `.{ .params = ... }`. CDP.sendEvent already places the payload
into the event's `params` field, so this produced a malformed wire event
with double-nested params (`params.params.frameId`) — unlike every sibling
frame event in the same function, which passes its fields flat.

CDP clients (e.g. Playwright via connectOverCDP + page.route) parse
frameId/parentFrameId at the top level of params. With the fields one level
too deep the client never registers the child frame, so when that frame's
Fetch.requestPaused arrives it is bare-continued instead of matched against
a route. The result: request interception (page.route / Fetch) silently does
not apply to iframe (sub-frame) document navigations — the iframe loads from
the real network. The interception layer itself was never at fault; it does
emit requestPaused for sub-frames.

Drop the extra wrapper so the payload is flat. Add a regression test that
dispatches frame_child_frame_created and asserts Page.frameAttached carries
frameId/parentFrameId directly under params (fails on the double-nested shape).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:11:20 +02:00
Karl Seguin
cd6c027dd9 Merge pull request #2807 from lightpanda-io/runner
design: Runner supports multi-pages
2026-06-26 08:36:07 +08:00
Halil Durak
f6fb653a35 prefer resolveNavigation at page, target and fetch 2026-06-25 18:06:08 +03:00
Halil Durak
7bf68a484d URL: replace ensureEncoded with resolve
Both essentially do the same; we can stick to `resolve` for further IDNA compat.
2026-06-25 18:05:47 +03:00
Karl Seguin
f2eaeb1c9a uaf: Prevent double-free on BrowserContext deinit with pending interception
Adds a abortParked (to be used instead of abort for a parked transfer) which
guards against a double-free by setting the state to .completing BEFORE running
the requestFailed callback. Without this, requestFail could itself cause the
transfer to be cleared.

Also added a guard to try to catch double transfer deinit's in Debug. Because
the memory could get re-used between the first and second free, the lack of
failure doesn't prove there is no UAF. But it's cheap to do and debug only.
2026-06-24 20:09:00 +08:00
Karl Seguin
39046492f1 design: Runner supports multi-pages
Follow up to https://github.com/lightpanda-io/browser/pull/2789 which adds
better multi-page support for runner. When waiting, callers have the choice
to wait for a specific frame, all current frames, or a given list of frames. The
last one, a given list of frames, is the most flexible, allowing callers to
provide an `until` per frame AND receive a per-frame result.

waitForSelector and waitForScript continue to be per frame (which is now
explicitly given).

Like 2789, while the changes aren't insignificant (Runner is doing important
work), a number of files were touched either purely because of changes in tests
or other superficial changes, e.g. `session.runner` is now infallible.
2026-06-24 17:44:58 +08:00
Karl Seguin
9482a9af98 Cleanup sloppy tests, Fix potential UAF, smaller cleanup (PR feedback).
Cleanup sloppy tests, make sure pages are always closed after page tests - not
before and not never.

Fix a UAF in replaceRootImmediate..if a page is being retired, also retire its
replacement.

Give BrowserContext a Session.PageHandle rather than having a frame_id and re-
implementing PageHandle logic.
2026-06-24 07:01:31 +08:00
Karl Seguin
da6cd66e36 design: multi-page session
Replaces the 1 (+1 inflight) page design of session for an unlimited number of
pages. The main goal is to support a more efficient async goto (1). Without this
commit, async goto has two implementation: multiple browser, which is resource
intensive (thread + isolate per page), bolting it onto sub-frames (like iframe
or popups). The issue with the 2nd version (which I originally suggested) is
that most resources are tied to the Page. So even if an async goto "page" (which
would map to a Frame) is released, most things stay in memory, including the
DOM (page.factory) and the V8::Context.

This new approach adds multiple page support to sessions. The advantage is
pretty clear: the existing memory model (page-tied resources) becomes a strength
of the design, rather than a weakness.

This change is not tirivla, but the diff is inflated by 2 large mechanial
changes, so it isn't _that_ big either. That said, I'd divide this into four
parts.

1 - The old concept of _active/_pending is now baked into the Page itself. A
Page has a `replaces: ?*Page = null` and `replacement: ?*Page = null` field.

2 - Because of #1 above, the Session now just has an `pages: ArrayList(*Page)`.
As much as possible, single-page APIs, like `removePage` no longer exists. We're
trying to present a consistent multi-page API. See #3.

3 - References to *Page and *Frame have always been dangerous. For example,
`lightpanda.fetch` has a block to scope `frame`:

```zig
{
    const frame = try session.createPage();
    // frame isn't safe to use after navigate, it can be swapped out
    _ = try frame.navigate(...)
}
```

While we still hand out *Page and *Frame (user-beware), some of the more
important APIs now take a frame_id which Session can resolve. Furthermore,
createPage now returns a PageHandle which is a safe wrapper around a page/frame.

4 - Two large mechanical changes were made:
  a - Many tests were superficially changed to account for new naming or use
      a new test-helper to preserve the single-page illusion (because 99% of
      tests _are_ single-page)
  b - Many CDP changes where `bc.session.currentFrame()` -> `bc.mainFrame()`).
      This isn't to say CDP changes are meaningless, but it's mostly 1 change
      about how the "main" target/frame_id is tracked (by CDP itself, rather
      than the Session) that required a number of superficial changes to
      accomodate

`Runner` remains largely single-page focused. Runner and some MCP/Agent tools
continue to be tied to the "currentFrame" or "primaryPage". The Runner is
something I want to address in a follow-up PR, but we need to figure out what
it means to "wait" for multiple pages.
As much as possible, Session becomes multi-page native and has no concept of
a special/first/primary/main page. Users of session become responsible for
tracking pages of interest.

This change is not trivial, but the diff is inflated by 2 large mechanical
changes:
1 - Many tests

(1) https://github.com/lightpanda-io/browser/pull/2759
2026-06-24 07:01:29 +08:00
Karl Seguin
48ed689c20 Merge pull request #2795 from lightpanda-io/innerText-visibility
webapi: innerText  uses StyleManager's visibility
2026-06-24 06:51:14 +08:00
Pierre Tachoire
5b97450cfa Merge pull request #2802 from lightpanda-io/feat/cdp-viewport-browser-level
cdp: return browser's viewport on getLayoutMetrics
2026-06-23 15:28:32 +00:00
Pierre Tachoire
82cd61e75c cdp: return browser's viewport on getLayoutMetrics 2026-06-23 15:22:16 +02:00
Pierre Tachoire
55568907ba Merge pull request #2791 from rohitsux/feat/cdp-viewport-browser-level
refactor(cdp): store viewport override at the browser level
2026-06-23 13:14:39 +00:00
Karl Seguin
23e72cf573 webapi: innerText uses StyleManager's visibility
innerText now uses the StyleManager's visibility check (isHidden) to skip
rendering children which are hidden.

Should allow us to close
https://github.com/lightpanda-io/browser/pull/2741 and
https://github.com/lightpanda-io/browser/issues/2734

Note that ~1000 WPT test still fail because of other style-driven behavior, most
significantly is `white-space`. The isHidden change only fixes a couple WPT
cases, but I think, for end users, it's the more important change (and it's also
the simpler for us to implement). Having wrong spacing is usually less of a
problem than showing the wrong content.
2026-06-22 18:26:31 +08:00
Pierre Tachoire
dc4ddf6913 cdp: fix log msg too long 2026-06-22 11:57:30 +02:00
Rohit
81f848d5d1 refactor(cdp): store viewport override at the browser level
Follow-up to #2664: move the Emulation.setDeviceMetricsOverride viewport
override from the Page to the Browser so it persists across page navigations
for the whole CDP connection, instead of being lost when the page is replaced.
Page.getViewport now delegates to Browser.getViewport, so every viewport
consumer keeps reading through a single path unchanged.
2026-06-22 14:20:15 +05:30
Rohit
8580f92488 feat(cdp): honor viewport in Emulation.setDeviceMetricsOverride
Now that #2694 centralized the default viewport, store an optional override
on the Page and have every viewport consumer read it through
Page.getViewport(): window.innerWidth/innerHeight, screen.width/height/
availWidth, visualViewport.width/height, Page.getLayoutMetrics, the
IntersectionObserver root rect, and matchMedia / the StyleManager media
cascade. This keeps the viewport consistent across all of them instead of
the split-brain a single-property override would create.

Emulation.setDeviceMetricsOverride writes the override (a 0 width or height
keeps that dimension, per the CDP default); clearDeviceMetricsOverride resets
to the default. Not-yet-emulated params (deviceScaleFactor, mobile, scale,
screenWidth/Height) are accepted but logged as not_implemented.
2026-06-20 07:18:12 +05:30
Karl Seguin
e9bbf95cf5 Merge pull request #2787 from lightpanda-io/frame_breakup
cleanup: Break various frame behavior into their own little utility f…
2026-06-20 07:09:00 +08:00
Pierre Tachoire
0f5ee79a1b Download events mustn't send session id 2026-06-19 14:31:40 +02:00
Pierre Tachoire
1f85ff1653 cdp: return session id with Browser.setDownloadBehavior 2026-06-19 11:51:42 +02:00
Armaan Sandhu
ab4d702445 address review: Browser-only download events, Header param parsing, real url/basename, deny default 2026-06-19 11:51:41 +02:00
Armaan Sandhu
8e8a9b2213 fix(cdp): make Browser.setDownloadBehavior a no-op without a loaded context
Playwright sends Browser.setDownloadBehavior at the browser level during
connection setup, before any target/context exists. Returning an error there
aborted the whole connection, failing every playwright demo-runner test.
Treat the no-context case as a success no-op, matching the prior behavior.
2026-06-19 11:50:31 +02:00
Armaan Sandhu
8ba54850db feat(cdp): implement Browser.setDownloadBehavior file downloads
Browser.setDownloadBehavior was a noop, so Lightpanda had no file-download
path. A response with Content-Disposition: attachment is now streamed to disk
under downloadPath, and Page.downloadWillBegin / Browser.downloadProgress are
emitted when eventsEnabled.

Fixes #2701
2026-06-19 11:50:29 +02:00
Karl Seguin
de5b9364e6 cleanup: Break various frame behavior into their own little utility files
This is purely mechanical and was driven almost entirely by Claude. The goal is
simply to break Frame.zig into small / more cohesive components. So instead of
having ~10 observer-specific methods on Frame, they now live in Frame.observers.

In general, our code always takes *Frame as the last parameter (a consequence
that this is the easiest / less ambiguous way to auto-inject a Frame into a
webapi). But, for these, I kept it as the first parameter because it's still
the "receiver" even though the dot syntax isn't applicable.
2026-06-19 10:11:17 +08:00
Rohit
a0fc38eb6d fix(cdp): store permissions at the browser level
Address review: permissions set via Browser.grantPermissions / setPermission
now live on the Browser instead of the active Page, so they persist across
page navigations (previously a navigation dropped them) and match how Chrome
scopes permissions to the browser context. navigator.permissions.query() reads
the browser-level state. Also log not_implemented for the origin and
browserContextId params, which are accepted but not yet honored.
2026-06-19 07:30:16 +08:00
Karl Seguin
3335c746dc switch permission value/state from string to enum 2026-06-19 07:28:36 +08:00
Rohit
697ae06852 feat(cdp): honor Browser.grantPermissions / setPermission / resetPermissions
These three Browser methods were noops, so a CDP client could not change
what navigator.permissions.query() reports - it always returned "prompt".

Store permission state on the active Page (keyed by permission name) and
have navigator.permissions.query() read it back. grantPermissions sets each
listed permission to "granted", setPermission sets a single permission to an
explicit state, and resetPermissions clears them (query falls back to
"prompt"). State is scoped to the Page and resets on navigation.

Adds a CDP round-trip test exercising all three methods.
2026-06-19 07:28:05 +08:00
Karl Seguin
297027f056 cdp: configurable max websocket and http message size
Move away from hard-coded 512KB (WS) and 4K (http) limits. Introduces two new
serve-specific command line arguments:

--cdp-max-message-size <INT>
        Maximum allowed incoming websocket message size.
        Defaults to 1048576 (1MB)
--cdp-max-http-message-size <INT>
        Maximum allowed HTTP request size
        Defaults to 4096 (maximum allowed: 16383)

--cdp-max-message-size has been bumped from 512KB to 1MB default.

Meant to provide a more robust solution than https://github.com/lightpanda-io/browser/pull/2717
2026-06-17 14:50:55 +08:00
Adrià Arrufat
2c35d760d8 Merge branch 'main' into agent 2026-06-15 15:21:54 +02:00
Karl Seguin
15d4968cc8 Merge pull request #2745 from lightpanda-io/silence_a_test_log
dev: silence a test that uses/needs console
2026-06-15 17:16:08 +08:00
Karl Seguin
78d7793327 dev: silence a test that uses/needs console 2026-06-15 16:59:04 +08:00
Karl Seguin
98dcd12a82 Merge pull request #2735 from lightpanda-io/cdp_setExtraHTTPHeaders
Cdp set extra http headers
2026-06-15 16:21:20 +08:00
Karl Seguin
3984082c3b Merge pull request #2700 from navidemad/fix-b14-sequential-focus
webapi: run sequential focus navigation on Tab
2026-06-15 06:34:52 +08:00
Karl Seguin
02115bef72 Merge pull request #2682 from navidemad/agent-discovery-signals
Surface server-side agent-discovery signals: Link response header (RFC 8288) + robots.txt Content-Signal
2026-06-14 20:31:47 +08:00
Adrià Arrufat
4147274fa8 Merge branch 'main' into agent 2026-06-14 10:28:21 +02:00
Karl Seguin
a4c5d09075 Merge pull request #2729 from navidemad/fix-a47-page-navigate-failed-response
cdp: answer Page.navigate with errorText when navigation fails
2026-06-14 13:39:43 +08:00
Karl Seguin
b957ad74c4 cdp, bot: Apply user-agent validation to CDP's setExtraHTTPHeaders
This is a change to https://github.com/lightpanda-io/browser/pull/2706 which
applies the Config.validateUserAgent to CDP's network.setExtraHTTPHeaders. This
makes the CDP method consistent with all other forms of user-agent setting, i.e
not allowing 'mozilla'-container values.
2026-06-13 15:43:38 +08:00
Adrià Arrufat
48025a371f Merge branch 'main' into agent 2026-06-13 08:17:31 +02:00
Navid EMAD
b136b3a84d cdp: report console.log/console.warn with their own consoleAPICalled types
console.log and console.warn were both dispatched as .info, so
Runtime.consoleAPICalled (and Console.messageAdded) reported type
"info" for all three of log/info/warn — clients filtering console
output by severity saw them collapsed into one bucket.

Add log/warning members to ConsoleMessageType (the protocol's wire
values — "warning", not "warn") and map console.log -> log,
console.warn -> warning. info/error/debug/trace already matched.

Closes #2730
2026-06-12 19:22:24 +02:00
Navid EMAD
c3afcc51dc cdp: answer Page.navigate with errorText when navigation fails
A root navigation that fails before any response headers arrive
(connection refused, DNS failure, TLS error) never answered the
Page.navigate command: the success reply is only sent from the
response-header path (frame_navigated), and frameErrorCallback dropped
the stored cdp_id along with the pending Page. The CDP client awaiting
the response was left hanging forever.

Dispatch a frame_navigate_failed notification from frameErrorCallback
when no response headers were processed (_http_status == null guards
against double-answering mid-body failures), and answer the pending
command with {frameId, loaderId, errorText} — matching Chrome, where
errorText is "present if and only if navigation has failed".

Closes #2728
2026-06-12 19:13:10 +02:00
Scott Taylor
723e7eb9d5 fix(cdp): validate User-Agent in Network.setExtraHTTPHeaders
A caller-supplied User-Agent overrides the built-in default and goes out
on the wire, so reject non-printable values the same way
Emulation.setUserAgentOverride and the --user-agent flag do.

We deliberately do NOT apply validateUserAgent's reserved (Mozilla)
check here: unlike the Emulation override (kept Lightpanda-branded to
keep go-rod working), setExtraHTTPHeaders is the intended,
Chrome-compatible escape hatch for setting a real browser-like
User-Agent, which by definition contains Mozilla.

Refs #2704
2026-06-12 12:28:19 -04:00
Adrià Arrufat
d9db947838 Merge branch 'main' into agent 2026-06-12 08:41:25 +02:00
Karl Seguin
e1851309f0 Merge pull request #2714 from navidemad/fix-a43-redirect-no-location
http: deliver 3xx responses without a Location header as final responses
2026-06-12 12:44:15 +08:00
Karl Seguin
3686fd2600 Stricter redirect status code check
Single Location header lookup
2026-06-12 11:47:27 +08:00