You captured a request or response from your own app and the body is binary: application/x-protobuf, application/grpc or application/grpc-web+proto. DevTools shows a row of unreadable bytes, and the .proto that describes them lives in another repository, or in a build you do not have checked out. protoc --decode_raw solves this if protoc is installed and you can get the bytes into a file. This page does the same in the browser: paste the bytes, read the fields.
What you get
Input. Paste the body as hex (spaces, colons, commas and 0x or \x prefixes are ignored) or base64 (standard or URL-safe, padding optional), or open the raw bytes as a file of up to 16 MiB. The input type is detected automatically: text made only of hex digits is read as hex, anything else as base64. If your base64 happens to contain only hex digits, pick Base64 by hand.
gRPC framing. gRPC and gRPC-Web bodies are not bare protobuf: each message is preceded by a 1-byte flag and a 4-byte big-endian length, and a streaming response holds several of them. When the bytes split cleanly into such frames, each frame is shown on its own:
- flag
0x00: a plain message, decoded; - flag
0x01: a compressed message. Payloads that start like gzip or zlib (grpc-encoding: gzipordeflate) are decompressed with the browser’s built-inDecompressionStreamand decoded; other encodings (snappy, zstd) are reported, not guessed at; - flag
0x80: the gRPC-Web trailers frame, shown as text (grpc-status,grpc-message).
A gRPC-Web-text body (application/grpc-web-text) is base64 of those frames, sometimes several padded base64 chunks back to back; paste it as is. Untick Detect gRPC / gRPC-Web frames to decode the bytes as a single message instead. A body that is one whole gzip stream is decompressed first.
Text view. The default view prints exactly what protoc --decode_raw prints: one field: value line per field, varints as unsigned decimal, fixed32 and fixed64 as zero-padded hex, nested messages in braces and everything else as a C-escaped string. It copies and downloads as .txt, so you can diff two captures with any text tool.
Tree view. The wire format records how a value was encoded, not what it means, so the tree shows the readings that fit:
- varint: unsigned, signed
int64(when the top bit is set), zigzagsintandboolfor 0 and 1; - fixed32 and fixed64: unsigned, signed, and
floatordouble; - length-delimited: nested message, UTF-8 string, packed varints, packed fixed32 or fixed64, and the bytes in hex. Buttons switch between the readings for each field, and the first one shown follows the same order protoc uses (message first).
- deprecated groups (wire types 3 and 4) are shown as nested fields, as protoc does.
Every field also shows its byte offset, so you can find it in a hex editor.
How it decides what a field is
protoc tries every non-empty length-delimited value as a nested message first. If the bytes parse completely as fields, it prints them as a message; otherwise it prints a string. That is why the two-byte string "hi" comes out as 1 { 13: 105 }: 0x68 is a valid tag (field 13, varint) and 0x69 is a valid value. The text view keeps protoc’s choice; the tree view lets you switch that field to string.
The decoder mirrors protoc’s two parsers. The top level is read the way the C++ ParseContext reads it (tags and lengths at most 5 bytes, a length below 2 GiB, groups nested at most 100 deep). Nested values are re-parsed the way TextFormat::Printer does, with a budget that starts at 10 and drops by one per level, so a message nested more than 10 deep prints as a string in protoc and here.
We checked this against libprotoc 36.2 on macOS. Ten test messages written for this page (every scalar type, packed and unpacked repeated fields, sint32 and sint64, strings with Chinese text and an emoji, maps, oneof, empty messages, field numbers up to 536,870,911, proto2 groups and a 12-level tree) were encoded with protoc --encode, and the text output matches protoc --decode_raw byte for byte. A further 1,000 generated and randomly mutated inputs, including over-long varints where protoc’s two parsers behave differently, gave the same text whenever protoc accepted the input and an error whenever it rejected it.
When the bytes are not valid protobuf
protoc stops with Failed to parse input. and prints nothing. This page decodes the fields before the problem, shows them, and names the byte offset and the reason: a truncated value, a 0x00 where a tag should be (often padding or a trailing checksum), field number 0, wire type 6 or 7, an end-group tag with no start, or a varint longer than 10 bytes. If nothing decodes at all, check that you pasted the body rather than the whole HTTP message, and try turning frame detection on or off.
How your data stays private
- Decoding runs in a Web Worker on your device. Nothing you paste or open is uploaded.
- Field values are shown as plain text; nothing from the payload is interpreted as HTML or run.
- Analytics events are coarse: success,
malformedor an error code, how the bytes arrived (hex,base64orfile), whether gRPC frames were found, a size bucket and a duration bucket. They never contain the bytes, field numbers or values. The full list is on the privacy page. - The tool page shows no ads.
How to verify this yourself
- Open your browser’s developer tools on the Network tab, then decode something. The only requests are page assets and the worker script
/_astro/protobuf-worker-*.js; none carries your input. - Or load the page, go offline (turn off Wi-Fi, or tick Offline in the Network tab) and decode. It still works.
- The worker runs under the Content Security Policy
default-src 'none'; script-src 'self'; connect-src 'none', so the browser does not let it make any network request.
Limitations
- No field names or types. Without the schema, field 2 is just field 2, and a
stringand abytesfield look the same. Decoding with a pasted.protois not offered: protobuf.js, the usual library for it, compiles its decoders withnew Function, which this page’s Content Security Policy blocks. - Ambiguous values stay ambiguous. A short string can parse as a message, and a packed list of small numbers can parse as a string. The tree lists the readings; you pick the one that matches what your API sends.
- Compression is limited to gzip and deflate frames, which the browser can decompress. Snappy and zstd frames are flagged but not decoded.
- Size. Up to 16 MiB of bytes. The tree shows the first 200,000 fields; the text view and its download are always complete.
Where the bytes come from
To capture the traffic of your own web app in the first place, record it in DevTools and export a HAR; the HAR / SAZ API summarizer groups the requests by endpoint with their content types, so the protobuf ones are easy to spot, and the HAR file analyzer guide explains how to read a capture and share it safely. For the wider workflow, from captured requests back to the code that sends them, see tracing a website’s API calls. Only capture and decode traffic from apps and services you own or are authorized to test.
Related guides
- Decode Protobuf Without the .proto: Wire Types and gRPC Frames: one message encoded with protoc and read back without its schema, line by line against
protoc --decode_raw.