capnwasm wire inspector

A standalone Cap'n Proto wire inspector hosted as a single file on this site. Not part of the npm package — you don't pay for it in production. Paste one line into DevTools when you need to see what's actually on the wire.

Try it in DevTools Console

  1. Open DevTools on this page.
  2. Paste the snippet below into the Console.
  3. Expand the logged tree to inspect the live Cap'n Proto bytes returned by /api/users/42.
const cw = await import( + "/inspect.js");
await cw.inspect(fetch("/api/users/42"));

The console gets an expandable tree: segment count, root pointer, struct shape, list elements, embedded text, and hex previews of binary data.

What inspect() accepts

cw.inspect(fetch("/api/users/42"));              // Promise<Response>
cw.inspect(response);                            // Response
cw.inspect(arrayBuffer);                         // ArrayBuffer
cw.inspect(uint8Array);                          // Uint8Array
cw.inspect(dataView);                            // DataView

Whatever shape you have, it figures out the bytes and walks them. Returns a Promise that resolves to the decoded structure (so you can also do const decoded = await cw.inspect(...)).

Schema-aware decode

Without a schema, the walker prints raw struct bytes and pointer kinds. Pass a generated reader and a loaded CapnCpp to get field names back:

import { UserReader } from "./user.capnp.gen.mjs";
import { load } from "capnwasm/browser";

const cpp = await load(...);

cw.inspect(fetch("/api/users/42"), { reader: UserReader, cpp });
// → { id: 42n, name: "Alice", email: "...", joinedAtMs: ..., active: true, avatar: Uint8Array }

What the schemaless walker shows

For a User message with id: 42, name: "Alice", email: "alice@…", active: true, avatar: <8 bytes>:

▾ Cap'n Proto frame  144 bytes, 1 segment(s)
   bytes: 144
   ▸ segments: [{ index: 0, words: 17, bytes: 136 }]
   ▾ root:
       kind: "struct"
       dataWords: 3
       ptrWords: 3
       data: "2a 00 00 00 00 00 00 00 00 00 c0 d3 50 ec 8b 01 …"
       ▸ dataDecoded: [{ word: 0, u64: 42n, ... }, ...]
       ▾ pointers:
           ▸ { kind: "text", length: 5, value: "Alice" }
           ▸ { kind: "text", length: 17, value: "alice@example.com" }
           ▸ { kind: "bytes", length: 8, preview: "01 02 03 04 05 06 07 08" }

Lists, composite-element lists, and far-pointer chasing all decode automatically. Cycles are guarded against (max depth 16).

CSP fallback

If the page you're debugging has a strict Content-Security-Policy: script-src 'self', dynamic import() from a different origin gets blocked. Fetch as text and evaluate locally instead:

const src = await fetch( + "/inspect.js").then(r => r.text());
const cw = await new Function("const m={};const exports=m;" + src + "; return m")();
cw.inspect(...);

For most apps the dynamic-import form works fine.

Why it's not in the npm package

If you want the file in your repo for offline use, grab it from this site once and commit it locally. Pure JS, no dependencies, ~17 KB / 5.5 KB gz. Walks the Cap'n Proto wire format directly — no wasm needed for the schemaless path. The schema-aware path delegates to whatever generated reader you pass in.