CrocProbe
Menu

Notes → iOS Web Inspector protocol

What iOS Safari's Web Inspector protocol does not tell you

Written 10 September 2026. Verified against CrocProbe 1.0.8.

Safari on iPhone speaks WebKit's remote inspector protocol, and ios-webkit-debug-proxy carries it over USB to a local port. None of what follows is documented. Every item on this page was established by probing a real iPhone on iOS 26.6, and it is written down here so that the next person can skip the probing.

CSS.enable poisons the session

Send CSS.enable to the page target and the target stops answering everything, including DOM calls that worked a second earlier. It does not merely fail. Nothing sent after it gets a reply.

Never send it. Computed styles are still available: call Runtime.callFunctionOn against the element and read getComputedStyle inside the function.

Frame targets acknowledge and never reply

The inspector channel is multi-target. Every command must be wrapped in Target.sendMessageToTarget, and every reply arrives inside Target.dispatchMessageFromTarget. The proxy does not unwrap either direction for you.

Only the page target answers. A page with iframes announces many targets. One news site announced fifteen, and eleven frame-* targets arrived before the single page-* target. Every frame target acknowledged the command envelope and then never replied.

A client that latches the first Target.targetCreated it sees therefore hangs on any page carrying iframes, which is most of the web. Wait for the target whose type is page and send commands to that one.

A background tab accepts commands and never runs them

iOS suspends JavaScript in tabs that are not on screen. Runtime.evaluate against a background tab is acknowledged at the envelope level and then silently never executes. There is no error. The command hangs indefinitely.

Only the tab actually showing on the phone is inspectable. The page is still listed, the session still opens, and the envelope is still acknowledged, so every signal a client normally reads as alive says yes. This is the single most confusing failure in the whole workflow. If you write a client, make its timeout message say so, because a generic timeout sends the reader to the wrong place.

The user-facing side of this problem has its own page: connected, listed, and running nothing.

DOM.getDocument is the real enable step

DOM.enable and DOM.getBoxModel do not exist. Both answer was not found.

DOM.getDocument is the de-facto enable step. It builds the backend's node map, and until it has been called once, DOM.requestNode, the bridge from a Runtime objectId to a DOM nodeId that every selection depends on, fails with an empty error message. Call it once before any node lookup.

DOM.requestChildNodes answers {} and delivers the nodes as a separate DOM.setChildNodes event. Wait for the event, not the reply.

Two related points come from reading WebKit's own InspectorDOMAgent rather than from probing. Calling DOM.getDocument again discards every node id issued before it, and using an old id afterwards fails with Missing node for given nodeId, or, for DOM.requestChildNodes, with nothing at all. And DOM.setChildNodes is sent once per document read: a second DOM.requestChildNodes for the same node answers {} and sends no event, because the backend assumes you kept the first delivery.

Smaller things that also cost an afternoon

Related