gRPC
Tinspec speaks native gRPC. This is one of the reasons it is a desktop app: a browser tab cannot, and gRPC-Web proxies change what you are testing.
Where the descriptor comes from
Section titled “Where the descriptor comes from”A gRPC call cannot be encoded without a service descriptor. The editor’s Descriptor picker offers three ways to get one:
| Source | What it reads | Who keeps it current |
|---|---|---|
| .proto file | A .proto in your workspace, compiled at send time | You do |
| Descriptor set | A compiled FileDescriptorSet — from protoc --descriptor_set_out, buf build -o, or saved from a reflection poll | You do |
| Server reflection | The running server’s own grpc.reflection.v1 service, with a v1alpha fallback | Nobody — the server answers for itself |
All three converge on a single decode path in the engine, so everything below the picker —
services, methods, message skeletons, google.api.http bindings — looks identical whichever
one you chose.
Switching sources never clears the other path. protoPath and descriptorSetPath are separate
fields on purpose, so source ↔ compiled ↔ live server is a flip rather than a retype.
Server reflection
Section titled “Server reflection”Point the URL field at the grpc://host:port target and press Reflect. No .proto, no
file to keep in sync with the service’s code.
It is a button rather than something that happens as you type, because the target is a live text field and describing on every keystroke would mean polling somebody’s server on every keystroke. Editing the target invalidates the poll rather than leaving the previous server’s method list on screen under a new URL.
The reflection call is an ordinary authenticated gRPC call on the same channel as the RPC
it describes — same metadata, same auth provider, same TLS material — so a service behind a
bearer token needs no special handling. A chip records which reflection service answered
(reflection · v1 or v1alpha), which turns “reflection behaved oddly” into “it answered over
the legacy service”.
A server with reflection switched off is named rather than dumped: This server does not expose reflection — point at a .proto or a compiled descriptor set instead.
Saving a poll for offline use
Section titled “Saving a poll for offline use”Save for offline use… writes that same poll’s descriptors to a .protoset file and offers
to point the request at it.
The copy is lossless by construction, not by effort: a reflected descriptor already is a
descriptor set, extension options included — which is where google.api.http annotations live.
So it is a byte copy, not an export that quietly drops things.
Set up a call
Section titled “Set up a call”- Create a request with protocol gRPC.
- Set the target —
grpc://host:50051, orgrpcs://host:443for TLS. (Reflection needs it first; the other two sources do not care.) - Pick a Descriptor source and load it — import a
.proto, choose a.protoset, or press Reflect. - Pick a service and method.
- Author the request message as JSON. It is encoded against the descriptor at send time.
Headers on a gRPC request are call metadata. {{variables}} resolve in the target, the
metadata, and the message body exactly as they do for HTTP.
Press Send. The response renders in the normal response pane, with the same JSON folding and history as any other request.
Streaming
Section titled “Streaming”Streaming methods are detected from the descriptor, and the editor changes shape accordingly:
| Method kind | What you get |
|---|---|
| Client-streaming | A list of messages to send in order, plus Connect / Send / Close-send controls |
| Server-streaming | One message editor, plus Connect and Close |
| Bidirectional | A message list and a compose row for sending interactively while the session is open |
Messages flow into a live log showing each frame as it arrives, in both directions.
Closing the send side and closing the session are separate actions, because for client-streaming they mean different things: half-close tells the server no more messages are coming and lets it reply.
Deadlines, message size, and compression
Section titled “Deadlines, message size, and compression”The effective timeout is also the call deadline: the engine turns it into a real
grpc-timeout header, so the server is told when to give up rather than only the client. 0
means no deadline. The global floor is Settings → Sending → Request timeout.
Message-size limits and compression are per-call too, under a grpc block in the request’s
send settings:
settings: timeoutMs: 30000 grpc: maxDecodingMessageBytes: 16777216 maxEncodingMessageBytes: 16777216 sendCompression: gzip acceptCompression: gzipnone, gzip and zstd are the three encodings, and none is spelled out rather than
omitted so a request can switch off an encoding a wider layer turned on.
grpcs:// targets connect using the platform’s web PKI roots.
Beyond that, gRPC gets the same TLS material HTTP does:
- Client certificates (mTLS) —
tls.clientCertRefandtls.clientKeyRef. Both are keychain names, never certificate contents; the engine resolves them at call time. Both halves or neither: one without the other is refused rather than connecting as an anonymous client, and a reference the keychain does not hold is named in the error. - A custom CA bundle —
tls.caBundlePath, a PEM file path. - The OS trust store —
tls.useOsTrustStore, which is the only way to trust a corporate MITM CA that the bundled roots cannot.
Settings → Network edits all four at the global floor.
For a development server with a self-signed certificate, turn on Allow insecure TLS in Settings → Sending. Handshake signatures are still checked; only the chain and name checks are waived. Combining it with a client certificate is refused rather than silently resolved — an insecure connection cannot present one.
Proxies do not apply to gRPC
Section titled “Proxies do not apply to gRPC”If a proxy is configured at any layer and the request is gRPC, the engine refuses the send with an error naming the proxy and the layer it came from. That is deliberate: bypassing a proxy the user configured would show up as an unexplained timeout much later. Add the host to the proxy’s no-proxy list, or clear the proxy for this request.
Trailers
Section titled “Trailers”gRPC status arrives in trailers, after the body — and they get their own Trailers tab in the response pane when the response has any. They are not folded into Headers because a trailers-only error puts its entire story there.
They are assertable with the ordinary header source: the engine falls back to trailers when
no header matches, so this works on success and on failure alike.
tests: - source: header path: grpc-status op: eq expected: "0"No Timeline for a gRPC call
Section titled “No Timeline for a gRPC call”Phase timings (DNS / TCP+TLS / TTFB / transfer) are collected only for buffered HTTP sends, so a gRPC response has no Timeline tab at all. An invented breakdown would be worse than none.
Chains
Section titled “Chains”Unary gRPC methods are chainable. Streaming ones are not — a chain step expects exactly one response.
The request picker greys out streaming methods when it can read the request’s .proto. For a
reflection- or protoset-backed request it cannot tell in advance, so the refusal comes from the
engine when the chain runs, naming the node and the method.
Through a gateway
Section titled “Through a gateway”If your gRPC service sits behind a gateway that transcodes gRPC to JSON — an Envoy
grpc_json_transcoder, for instance — Tinspec can send the mapped HTTP call instead of a
native gRPC one, for unary methods that carry a google.api.http annotation.
Mark the route matcher as transcoding (Envoy configs are auto-detected) and the gRPC editor shows a chip like REST via gateway — GET /v1/users/{id} so you can see which path the call will actually take. Streaming methods and unannotated methods fall back to native gRPC.
Bind a whole service as a source
Section titled “Bind a whole service as a source”A single request is one method. Add API source → gRPC service binds the whole service
instead: pick Server reflection (or a .proto), set a poll interval, and every method
materializes as an endpoint in the sidebar, grouped by service.
Because it is a spec source like any other, a reflected service inherits Spec Changes, the breaking-change classification and endpoint overlays for free — a method that changed shape this morning reads as a change, not as a stale import.
Two limits worth knowing:
- A
.proto-backed provider gets no schema diff. There is no fetched document and no live source to re-ask, so it gets none rather than a guessed one. - A reflection poll costs two reflection round trips — one to build the endpoints, one to project the result for the differ. It is the one place this feature pays twice.
In the file format
Section titled “In the file format”- name: Get user protocol: grpc method: POST url: grpc://localhost:50051 grpc: protoPath: proto/users.proto service: acme.users.v1.UserService method: GetUser message: '{"id":"{{userId}}"}'method: POST is required by the format even for gRPC. Streaming mode is not stored — it is
read from the descriptor.
descriptorSource is absent for a .proto, because absent already means proto. A
request written before the other two sources existed keeps its exact bytes. The other two spell
it out:
grpc: descriptorSource: descriptorSet descriptorSetPath: proto/users.protoset service: acme.users.v1.UserService method: GetUser grpc: descriptorSource: reflection service: acme.users.v1.UserService method: GetUser