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.zig — CdpClient 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:56 — Target.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.
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 thattab_idreturns: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 cachedCdpClientfor that tab that's broken.example.comworks 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
Hypothesis
Instagram triggers Chrome's site isolation to swap renderers because the page:
https://www.facebook.com/instagram/login_sync/as a cross-origin iframeIGDAWMainV4WebWorkershows up as its own target in/json)When that happens, Chrome fires
Target.detachedFromTargetand a new sessionId is attached to the target. Kuri's per-tab WebSocket (opened on the tab's originalwebSocketDebuggerUrl) 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 returnsCDP command failedwithout invalidating the cached client or refetching/jsonto get the newwebSocketDebuggerUrl.Evidence
GET /tabsfrom 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/jsondirectly shows the same target id with"title":"Instagram"— the page is alive/jsonalso lists an iframe target forhttps://www.facebook.com/instagram/login_sync/and a service worker target — both consistent with a site-isolation renderer swapWhere the source looks vulnerable
Browsed
mainbriefly. Some pointers for where the missing handling would go:src/cdp/client.zig—CdpClientconnects to a singlecdp_url(a per-targetwebSocketDebuggerUrl) and caches the connection. There is no listener forTarget.detachedFromTarget/Target.attachedToTarget, and nosessionIdplumbing anywhere in the codebase (grep for both turns up zero hits). Whenws.sendTextorreceiveMessageAllocfails,connectWs()reconnects to the same storedcdp_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 atab_id, theCdpClientis cached forever inself.cdp_clients. There is no invalidation path when the underlying target's session changes. Thews_urlis captured at tab discovery time from/jsonand never refreshed.src/cdp/protocol.zig:56—Target.attachToTargetis defined as a method name constant but isn't called fromclient.zigorbridge.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
webSocketDebuggerUrlfrom/json/listand rebuild theCdpClientbefore returning 502. A more correct fix is to attach via the browser endpoint and handleTarget.attachedToTarget/Target.detachedFromTargetwith proper sessionId routing in every CDP command frame.Environment
install.shfrom main).deb). Not a snap-specific issue.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.