Skip to content

CDP session silently dies after navigating to instagram.com (target detach not handled) #172

@pairie-koh

Description

@pairie-koh

Symptom

After navigating a tab to https://www.instagram.com/, kuri's CDP WebSocket for that tab becomes silently dead. Every subsequent CDP-backed endpoint on that tab_id returns:

HTTP 502 {"error":"CDP command failed"}

forever, until kuri is restarted. Kuri's own logs show nothing — it doesn't realize the session is stale. The tab is still alive in Chrome (confirmed via raw :9222/json), it's just kuri's cached CdpClient for that tab that's broken.

example.com works fine before AND after. Instagram specifically kills the session. Restarting kuri without restarting Chrome doesn't help — the stale ws_url gets restored from state and the same target is dead from kuri's perspective.

Repro

KURI_API_TOKEN=devtoken HEADLESS=false kuri &
# B = base URL with auth, T = a tab_id from GET /tabs

curl -G "$B/evaluate?tab_id=$T&expression=1%2B1"
# 200, returns "2" — CDP works

curl -G "$B/navigate?tab_id=$T&url=https://example.com"
# 200

curl -G "$B/get?tab_id=$T&type=url"
# "https://example.com/" — CDP still works

curl -G "$B/navigate?tab_id=$T&url=https://www.instagram.com/"
# 200 (no error returned)

curl -G "$B/evaluate?tab_id=$T&expression=1%2B1"
# 502 {"error":"CDP command failed"}
# every CDP-backed endpoint (/evaluate, /get, /navigate, /snapshot, /screenshot) now 502s forever

Hypothesis

Instagram triggers Chrome's site isolation to swap renderers because the page:

  • embeds https://www.facebook.com/instagram/login_sync/ as a cross-origin iframe
  • registers a service worker (IGDAWMainV4WebWorker shows up as its own target in /json)

When that happens, Chrome fires Target.detachedFromTarget and a new sessionId is attached to the target. Kuri's per-tab WebSocket (opened on the tab's original webSocketDebuggerUrl) is now bound to a session Chrome no longer serves. Commands either error out without surfacing the cause, or read the wrong response stream — kuri just sees the send/receive fail and returns CDP command failed without invalidating the cached client or refetching /json to get the new webSocketDebuggerUrl.

Evidence

  • GET /tabs from kuri returns: {"id":"<X>","url":"https://www.instagram.com/","title":"New Tab"} — title is stale (kuri stopped receiving Target.targetInfoChanged events for the tab)
  • GET http://127.0.0.1:9222/json directly shows the same target id with "title":"Instagram" — the page is alive
  • /json also lists an iframe target for https://www.facebook.com/instagram/login_sync/ and a service worker target — both consistent with a site-isolation renderer swap

Where the source looks vulnerable

Browsed main briefly. Some pointers for where the missing handling would go:

  • src/cdp/client.zigCdpClient connects to a single cdp_url (a per-target webSocketDebuggerUrl) and caches the connection. There is no listener for Target.detachedFromTarget / Target.attachedToTarget, and no sessionId plumbing anywhere in the codebase (grep for both turns up zero hits). When ws.sendText or receiveMessageAlloc fails, connectWs() reconnects to the same stored cdp_url — but if Chrome detached that session, the URL is dead and the reconnect either fails or yields a session that immediately gets refused.
  • src/bridge/bridge.zig:316 (getCdpClient) — once created for a tab_id, the CdpClient is cached forever in self.cdp_clients. There is no invalidation path when the underlying target's session changes. The ws_url is captured at tab discovery time from /json and never refreshed.
  • src/cdp/protocol.zig:56Target.attachToTarget is defined as a method name constant but isn't called from client.zig or bridge.zig. Kuri appears to rely on per-target WS endpoints (/devtools/page/<id>) instead of attaching via a browser-wide socket, which is exactly the configuration that breaks on cross-process renderer swaps.

A minimal fix is probably: on a failed send/receive, refresh the tab's webSocketDebuggerUrl from /json/list and rebuild the CdpClient before returning 502. A more correct fix is to attach via the browser endpoint and handle Target.attachedToTarget / Target.detachedFromTarget with proper sessionId routing in every CDP command frame.

Environment

  • kuri v0.4.1 (installed via install.sh from main)
  • Reproduces on both snap-packaged chromium (Ubuntu 24.04) AND Google Chrome 148 (.deb). Not a snap-specific issue.
  • Host: Windows 11 + WSL2 Ubuntu 24.04. Kuri runs in WSL.
  • HEADLESS=false — chromium spawned by kuri renders via WSLg.

Impact

This blocks any Instagram scraping / automation use case on kuri. Any site that triggers a cross-process renderer swap during navigation (cross-origin iframes from another site, service-worker registration, COOP/COEP isolation) will likely hit the same failure mode — IG is just an easy repro.

Happy to test patches.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions