Skip to content

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.

A gRPC call cannot be encoded without a service descriptor. The editor’s Descriptor picker offers three ways to get one:

SourceWhat it readsWho keeps it current
.proto fileA .proto in your workspace, compiled at send timeYou do
Descriptor setA compiled FileDescriptorSet — from protoc --descriptor_set_out, buf build -o, or saved from a reflection pollYou do
Server reflectionThe running server’s own grpc.reflection.v1 service, with a v1alpha fallbackNobody — 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.

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.

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.

  1. Create a request with protocol gRPC.
  2. Set the target — grpc://host:50051, or grpcs://host:443 for TLS. (Reflection needs it first; the other two sources do not care.)
  3. Pick a Descriptor source and load it — import a .proto, choose a .protoset, or press Reflect.
  4. Pick a service and method.
  5. 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 methods are detected from the descriptor, and the editor changes shape accordingly:

Method kindWhat you get
Client-streamingA list of messages to send in order, plus Connect / Send / Close-send controls
Server-streamingOne message editor, plus Connect and Close
BidirectionalA 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.

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: gzip

none, 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.clientCertRef and tls.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 bundletls.caBundlePath, a PEM file path.
  • The OS trust storetls.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.

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.

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"

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.

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.

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.

See Gateway route matchers.

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.
- 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