|
| 1 | +# Inspector Proxy Protocol |
| 2 | + |
| 3 | +[🏠 Home](../../../../../__docs__/README.md) |
| 4 | + |
| 5 | +The inspector-proxy protocol facilitates Chrome DevTools Protocol (CDP) target |
| 6 | +discovery and communication between **debuggers** (e.g., Chrome DevTools, VS |
| 7 | +Code) and **devices** (processes containing React Native hosts). The proxy |
| 8 | +multiplexes connections over a single WebSocket per device, allowing multiple |
| 9 | +debuggers to connect to multiple pages on the same device. |
| 10 | + |
| 11 | +## 🚀 Usage |
| 12 | + |
| 13 | +### Target Discovery (HTTP) |
| 14 | + |
| 15 | +We implement a subset of the |
| 16 | +[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)'s |
| 17 | +[HTTP endpoints](https://chromedevtools.github.io/devtools-protocol/#:~:text=a%20reconnect%20button.-,HTTP%20Endpoints,-If%20started%20with) |
| 18 | +to allow debuggers to discover targets. |
| 19 | + |
| 20 | +| Endpoint | Description | |
| 21 | +| --------------------------- | ------------------------ | |
| 22 | +| `GET /json` or `/json/list` | List of debuggable pages | |
| 23 | +| `GET /json/version` | Protocol version info | |
| 24 | + |
| 25 | +### Device Registration (WebSocket) |
| 26 | + |
| 27 | +Devices register themselves with the proxy by connecting to `/inspector/device`: |
| 28 | + |
| 29 | +```text |
| 30 | +ws://{host}/inspector/device?device={id}&name={name}&app={bundle_id}&profiling={true|false} |
| 31 | +``` |
| 32 | + |
| 33 | +| Parameter | Required | Description | |
| 34 | +| ----------- | -------- | ------------------------------------------------------------ | |
| 35 | +| `device` | No\* | Logical device identifier. Auto-generated if omitted. | |
| 36 | +| `name` | No | Human-readable device name. Defaults to "Unknown". | |
| 37 | +| `app` | No | App bundle identifier. Defaults to "Unknown". | |
| 38 | +| `profiling` | No | "true" if this is a profiling build. (Used for logging only) | |
| 39 | + |
| 40 | +\*Recommended for connection persistence across app restarts. |
| 41 | + |
| 42 | +#### Requirements for the `device` parameter |
| 43 | + |
| 44 | +The intent of the logical device ID is to help with target discovery and |
| 45 | +especially *re*discovery - to reduce the number of times users need to |
| 46 | +explicitly close and restart the debugger frontend (e.g. after an app crash). |
| 47 | + |
| 48 | +If provided, the logical device ID: |
| 49 | + |
| 50 | +1. SHOULD be stable for the current combination of physical device (or emulator |
| 51 | + instance) and app. |
| 52 | +2. SHOULD be stable across installs/launches of the same app on the same device |
| 53 | + (or emulator instance), though it MAY be user-resettable (so as to not |
| 54 | + require any special privacy permissions). |
| 55 | +3. MUST be unique across different apps on the same physical device (or |
| 56 | + emulator). |
| 57 | +4. MUST be unique across physical devices (or emulators). |
| 58 | +5. MUST be unique for each concurrent _instance_ of the same app on the same |
| 59 | + physical device (or emulator). |
| 60 | + |
| 61 | +NOTE: The uniqueness requirements are stronger (MUST) than the stability |
| 62 | +requirements (SHOULD). In particular, on platforms that allow multiple instances |
| 63 | +of the same app to run concurrently, requirements 1 and/or 2 MAY be violated in |
| 64 | +order to meet requirement 5. This is relevant, for example, on desktop |
| 65 | +platforms. |
| 66 | + |
| 67 | +### Debugger Connection (WebSocket) |
| 68 | + |
| 69 | +Debuggers connect to `/inspector/debug` to form a CDP session with a page: |
| 70 | + |
| 71 | +```text |
| 72 | +ws://{host}/inspector/debug?device={device_id}&page={page_id} |
| 73 | +``` |
| 74 | + |
| 75 | +Both `device` and `page` query parameters are required. |
| 76 | + |
| 77 | +## 📐 Design |
| 78 | + |
| 79 | +### Architecture |
| 80 | + |
| 81 | +```text |
| 82 | +┌─────────────────┐ ┌─────────────────────────┐ ┌────────────────┐ |
| 83 | +│ Debugger │────▶│ Inspector Proxy │◀────│ Device │ |
| 84 | +│ (Chrome/VSCode) │ │ (Node.js) │ │ (iOS/Android) │ |
| 85 | +└─────────────────┘ └─────────────────────────┘ └────────────────┘ |
| 86 | + WebSocket HTTP + WebSocket WebSocket |
| 87 | + /inspector/debug /json, /json/list /inspector/device |
| 88 | + /json/version |
| 89 | +``` |
| 90 | + |
| 91 | +### Device ↔ Proxy Protocol |
| 92 | + |
| 93 | +All messages are JSON-encoded WebSocket text frames: |
| 94 | + |
| 95 | +```typescript |
| 96 | +interface Message { |
| 97 | + event: string; |
| 98 | + payload?: /* depends on event */; |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +#### Proxy → Device Messages |
| 103 | + |
| 104 | +| Event | Payload | Description | |
| 105 | +| -------------- | ------------------------------------------ | --------------------------------------------- | |
| 106 | +| `getPages` | _(none)_ | Request current page list. Sent periodically. | |
| 107 | +| `connect` | `{ pageId: string }` | Prepare for debugger connection to page. | |
| 108 | +| `disconnect` | `{ pageId: string }` | Terminate debugger session for page. | |
| 109 | +| `wrappedEvent` | `{ pageId: string, wrappedEvent: string }` | Forward CDP message (JSON string) to page. | |
| 110 | + |
| 111 | +#### Device → Proxy Messages |
| 112 | + |
| 113 | +| Event | Payload | Description | |
| 114 | +| -------------- | ------------------------------------------ | ----------------------------------------------------- | |
| 115 | +| `getPages` | `Page[]` | Current list of inspectable pages. | |
| 116 | +| `disconnect` | `{ pageId: string }` | Notify that page disconnected or rejected connection. | |
| 117 | +| `wrappedEvent` | `{ pageId: string, wrappedEvent: string }` | Forward CDP message (JSON string) from page. | |
| 118 | + |
| 119 | +#### Page Object |
| 120 | + |
| 121 | +```typescript |
| 122 | +interface Page { |
| 123 | + id: string; // Unique page identifier (typically numeric string) |
| 124 | + title: string; // Display title |
| 125 | + app: string; // App bundle identifier |
| 126 | + description?: string; // Additional description |
| 127 | + capabilities?: { |
| 128 | + nativePageReloads?: boolean; // Target keeps the socket open across reloads |
| 129 | + nativeSourceCodeFetching?: boolean; // Target supports Network.loadNetworkResource |
| 130 | + prefersFuseboxFrontend?: boolean; // Target is designed for React Native DevTools |
| 131 | + }; |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +### Connection Lifecycle |
| 136 | + |
| 137 | +**Device Registration:** |
| 138 | + |
| 139 | +```text |
| 140 | +Device Proxy |
| 141 | + │ │ |
| 142 | + │──── WS Connect ─────────────────▶│ |
| 143 | + │ /inspector/device?... │ |
| 144 | + │ │ |
| 145 | + │◀──── getPages ───────────────────│ (periodically) |
| 146 | + │ │ |
| 147 | + │───── getPages response ─────────▶│ |
| 148 | + │ (page list) │ |
| 149 | +``` |
| 150 | + |
| 151 | +**Debugger Session:** |
| 152 | + |
| 153 | +```text |
| 154 | +Debugger Proxy Device |
| 155 | + │ │ │ |
| 156 | + │── WS Connect ───▶│ │ |
| 157 | + │ ?device&page │── connect ────────────────▶│ |
| 158 | + │ │ {pageId} │ |
| 159 | + │ │ │ |
| 160 | + │── CDP Request ──▶│── wrappedEvent ───────────▶│ |
| 161 | + │ │ {pageId, wrappedEvent} │ |
| 162 | + │ │ │ |
| 163 | + │ │◀── wrappedEvent ───────────│ |
| 164 | + │◀── CDP Response ─│ {pageId, wrappedEvent} │ |
| 165 | + │ │ │ |
| 166 | + │── WS Close ─────▶│── disconnect ─────────────▶│ |
| 167 | + │ │ {pageId} │ |
| 168 | +``` |
| 169 | + |
| 170 | +**Connection Rejection:** |
| 171 | + |
| 172 | +If a device cannot accept a `connect` (e.g., page doesn't exist), it should send |
| 173 | +a `disconnect` back to the proxy for that `pageId`. |
| 174 | + |
| 175 | +### Connection Semantics |
| 176 | + |
| 177 | +1. **One Debugger Per Page**: New debugger connections to an already-connected |
| 178 | + page disconnect the existing debugger. |
| 179 | + |
| 180 | +2. **Device Reconnection**: If a device reconnects with the same `device` ID, |
| 181 | + the proxy may attempt to preserve active debugger sessions by forwarding them |
| 182 | + to the new device connection. |
| 183 | + |
| 184 | +### WebSocket Close Reasons |
| 185 | + |
| 186 | +The proxy uses specific close reasons that DevTools frontends may recognize: |
| 187 | + |
| 188 | +| Reason | Context | |
| 189 | +| ----------------------- | --------------------------------------- | |
| 190 | +| `[PAGE_NOT_FOUND]` | Debugger connected to non-existent page | |
| 191 | +| `[CONNECTION_LOST]` | Device disconnected | |
| 192 | +| `[RECREATING_DEVICE]` | Device is reconnecting | |
| 193 | +| `[NEW_DEBUGGER_OPENED]` | Another debugger took over this page | |
| 194 | +| `[UNREGISTERED_DEVICE]` | Device ID not found | |
| 195 | +| `[INCORRECT_URL]` | Missing device/page query parameters | |
| 196 | + |
| 197 | +### PageDescription (HTTP Response) |
| 198 | + |
| 199 | +The `/json` endpoint returns enriched page descriptions based on those reported |
| 200 | +by the device. |
| 201 | + |
| 202 | +```typescript |
| 203 | +interface PageDescription { |
| 204 | + // Used for target selection |
| 205 | + id: string; // "{deviceId}-{pageId}" |
| 206 | + |
| 207 | + // Used for display |
| 208 | + title: string; |
| 209 | + description: string; |
| 210 | + deviceName: string; |
| 211 | + |
| 212 | + // Used for target matching |
| 213 | + appId: string; |
| 214 | + |
| 215 | + // Used for debugger connection |
| 216 | + webSocketDebuggerUrl: string; |
| 217 | + |
| 218 | + // React Native-specific metadata |
| 219 | + reactNative: { |
| 220 | + logicalDeviceId: string; // Used for target matching |
| 221 | + capabilities: { |
| 222 | + nativePageReloads?: boolean; // Used for target filtering |
| 223 | + prefersFuseboxFrontend?: boolean; // Used for frontend selection |
| 224 | + }; |
| 225 | + }; |
| 226 | +} |
| 227 | +``` |
| 228 | + |
| 229 | +## 🔗 Relationship with other systems |
| 230 | + |
| 231 | +### Part of this |
| 232 | + |
| 233 | +- **Device.js** - Per-device connection handler in the proxy |
| 234 | +- **InspectorProxy.js** - Main proxy HTTP/WebSocket server |
| 235 | + |
| 236 | +### Used by this |
| 237 | + |
| 238 | +- **Chrome DevTools Protocol (CDP)** - The wrapped messages are CDP messages |
| 239 | + exchanged between DevTools frontends and JavaScript runtimes. |
| 240 | +- **WebSocket** - Transport layer for device and debugger connections. |
| 241 | + |
| 242 | +### Uses this |
| 243 | + |
| 244 | +- **InspectorPackagerConnection (C++)** - Shared device-side protocol |
| 245 | + implementation in `ReactCommon/jsinspector-modern/`. |
| 246 | +- **Platform layers** - iOS (`RCTInspectorDevServerHelper.mm`), Android |
| 247 | + (`DevServerHelper.kt`), and ReactCxxPlatform (`Inspector.cpp`) provide |
| 248 | + WebSocket I/O and threading. |
| 249 | +- **openDebuggerMiddleware** - Uses `/json` to discover targets for the |
| 250 | + `/open-debugger` endpoint. |
| 251 | +- **OpenDebuggerKeyboardHandler** - Uses `/json` to display target selection in |
| 252 | + the CLI. |
| 253 | + |
| 254 | +--- |
| 255 | + |
| 256 | +## Legacy Features |
| 257 | + |
| 258 | +The following features exist for backward compatibility with older React Native |
| 259 | +targets that lack modern capabilities. New implementations should set |
| 260 | +appropriate capability flags and may ignore this section. |
| 261 | + |
| 262 | +### Synthetic Reloadable Page (Page ID `-1`) |
| 263 | + |
| 264 | +For targets without the `nativePageReloads` capability, the proxy exposes a |
| 265 | +synthetic page with ID `-1` titled "React Native Experimental (Improved Chrome |
| 266 | +Reloads)". Debuggers connecting to this page are automatically redirected to the |
| 267 | +most recent React Native page, surviving page reloads. |
| 268 | + |
| 269 | +When a new React Native page appears while a debugger is connected to `-1`: |
| 270 | + |
| 271 | +1. Proxy sends `disconnect` for the old page, `connect` for the new page |
| 272 | +2. Proxy sends `Runtime.enable` and `Debugger.enable` CDP commands to the new |
| 273 | + page |
| 274 | +3. When `Runtime.executionContextCreated` is received, proxy sends |
| 275 | + `Runtime.executionContextsCleared` to debugger, then `Debugger.resume` to |
| 276 | + device |
| 277 | + |
| 278 | +### URL Rewriting |
| 279 | + |
| 280 | +For targets without the `nativeSourceCodeFetching` capability, the proxy |
| 281 | +rewrites URLs in CDP messages: |
| 282 | + |
| 283 | +- **Debugger.scriptParsed** (device → debugger): Device-relative URLs are |
| 284 | + rewritten to debugger-relative URLs |
| 285 | +- **Debugger.setBreakpointByUrl** (debugger → device): URLs are rewritten back |
| 286 | + to device-relative form |
| 287 | +- **Debugger.getScriptSource**: Intercepted and handled by proxy via HTTP fetch |
| 288 | +- **Network.loadNetworkResource**: Returns CDP error (code -32601) to force |
| 289 | + frontend fallback |
| 290 | + |
| 291 | +Additionally, if a script URL matches `^[0-9a-z]+$` (alphanumeric ID), the proxy |
| 292 | +prepends `file://` to ensure Chrome downloads source maps. |
| 293 | + |
| 294 | +### Legacy Reload Notification |
| 295 | + |
| 296 | +For targets without `nativePageReloads`, when a `disconnect` event is received |
| 297 | +for a page, the proxy sends `{method: 'reload'}` to the connected debugger to |
| 298 | +signal a page reload. |
0 commit comments