Beyond HTTP

gRPC testing with a GUI, a file, and a CI runner.

Bandura is a gRPC testing tool with a desktop GUI and a headless runner, and it needs no account. A grpc node calls a unary or server-streaming method, described by a local .proto file or fetched over server reflection, and stores a response you assert on and capture from like any HTTP call. The test is one plain-YAML .aether file in your repo, edited on a graph canvas or as code, and the same engine runs it in the desktop app, the CLI, and CI. WebSocket exchanges and Server-Sent Event streams live in the same file, so one suite covers all four protocols.

v1.0.0-rc.8 is out now on Windows and Linux. v1.0.0 final, and signed macOS builds, in August 2026.

Download for Windows or Linux
or

One email when the signed macOS build and 1.0.0 ship, no reselling, opt out any time.

Windows · Linux · macOS when notarized  ·  The grpc node ↓  ·  WebSocket ↓  ·  Server-Sent Events ↓

The grpc node

Unary and server-streaming, from proto or reflection.

Describe the service one of two ways: point proto at a .proto file checked into the repo (with importPaths for its imports), or set reflection: true and let Bandura ask the server. Exactly one of the two, so a flow never carries an ambiguous service description. Every field the node accepts is listed in the node reference.

- id: get_user
  type: grpc
  config:
    target: ${{ variables.grpcHost }}   # host:port, no scheme
    service: user.v1.UserService
    method: GetUser
    proto: ./protos/user.proto          # ...or `reflection: true`
    message:
      id: ${{ variables.userId }}
    metadata:
      authorization: Bearer ${{ variables.token }}
    deadline: 10000
    capture:
      userName: response.body.name
  next: verify_user

- id: verify_user
  type: assertion
  config:
    check: 'response.status === 0 && variables.userName.length > 0'
  next: null

The response is a value, not a printout

response.status is the gRPC status code (0 is OK), response.body is the decoded message, or an array of messages for a server-streaming method. A non-OK status is not a failure by itself: NOT_FOUND is a legitimate thing to test for, so you assert response.status === 0 the way you assert a 2xx.

Metadata, TLS, deadline, retry

metadata carries auth the way headers do, tls: true secures the channel, and the per-call knob is deadline. The same retry block every request node takes works here too, so polling a method until a job settles is configuration, not code. A flow-level auth block covers the token the metadata carries, and mTLS and proxies are declared the same way.

The limit, stated plainly

Client-streaming and bidirectional-streaming methods are not supported yet. If your suite leans on either, Bandura today does not cover it, and this page would rather say so than sell you a checkmark.

One suite, four protocols

The gRPC call is a step, not a silo.

Every step stores { status, headers, body }, so values move between protocols without glue code: a REST login's captured token lands in gRPC metadata, a gRPC response feeds a WebSocket assertion, and flow-level hooks run around every request-like node for signing and correlation ids.

The Bandura desktop app after a passing run: a checkout flow on the graph canvas with green borders on the passed nodes, and the inspector open on a request node's result The Bandura desktop app after a passing run: a checkout flow on the graph canvas with green borders on the passed nodes, and the inspector open on a request node's result
A flow after a run: each node's routing is a field in the YAML, and the canvas draws it. This capture is an HTTP flow; a grpc or websocket node sits on the canvas exactly like these request nodes.

WebSocket

Open, send, collect, close: one node.

The exchange most suites need is a handshake, one message out, and some frames back. A websocket node does exactly that: headers and optional subprotocols on the handshake, an optional send, then frames collected until expectMessages is reached or a waitFor predicate is truthy.

- id: subscribe_prices
  type: websocket
  config:
    url: ${{ variables.wsUrl }}/stream
    send: '{ "type": "subscribe", "channel": "prices" }'
    waitFor: "message.type === 'data'"
    capture:
      price: response.body[0].price
  next: verify

Frames you can assert on

response.body is the array of received frames, parsed as JSON where possible, and response.status is the close code (1000 is clean). If the expected frames do not arrive before the timeout, the node fails and says what it did receive, instead of a suite that hangs in CI. Retry works here as well, and flow hooks wrap the node like any other call.

Server-Sent Events

Assert on a stream the way you assert on a body.

SSE is the protocol behind most streaming LLM endpoints and live-update feeds. An sse block on an ordinary request node collects the event stream into response.body as an array of { event, data } objects, data parsed as JSON where it is JSON, so a [DONE] sentinel stays a string while streamed deltas arrive as objects.

- id: stream_completion
  type: request
  config:
    method: POST
    url: ${{ variables.baseUrl }}/v1/chat
    body: '{ "model": "m", "stream": true }'
    sse:
      waitFor: 'event.data === "[DONE]"'
      timeout: 120000
    capture:
      chunks: response.body.length
  next: verify

Short reads fail, on purpose

Collection stops at whichever comes first: expectEvents, a truthy waitFor, the stream ending, or the timeout. If you asked for something specific and the stream ends without it, the node fails and reports what arrived. A short read that quietly looks like a pass is exactly the failure this exists to catch. Use sse.timeout for streams rather than the request's own timeout, which would cut a healthy long-lived stream short.

Automate in CI

The same four protocols, headless.

The CLI is a compiled binary with the engine inside: npm install -g @bandura/cli, then bandura run flows/ executes gRPC, WebSocket and SSE steps exactly as the desktop app does, with deterministic exit codes, JUnit XML, and GitHub annotations on the pull request. macOS can run the CLI today, ahead of the signed desktop build. API tests in CI →

Questions, answered straight

FAQ

Does Bandura support gRPC streaming?

Server-streaming, yes: response.body becomes an array of decoded messages you can assert on and capture from. Client-streaming and bidirectional-streaming methods are not supported yet, and this page says so rather than hiding it under a protocols checkmark. Unary calls are fully supported, with metadata, TLS to the target, a per-call deadline, and retry.

Do I need a .proto file to test a gRPC service?

No, if the server exposes reflection. A grpc node takes exactly one of proto (a local .proto file, with importPaths for its imports) or reflection: true, in which case Bandura fetches the service description from the server itself. Reflection is the faster start; a checked-in .proto keeps the test self-contained and reviewable, which is usually what a repo wants.

How do gRPC status codes work in assertions?

response.status is the gRPC status code, 0 meaning OK, and a non-OK status is deliberately not a failure on its own: NOT_FOUND is a legitimate thing to test for. Assert response.status === 0 when success is what you expect, exactly as you assert a 2xx on HTTP. The decoded response message is response.body, or an array of messages for a server-streaming method.

Can Bandura test WebSocket connections?

Yes, as one node covering the whole exchange: open the socket (ws:// or wss://, with handshake headers and optional subprotocols), optionally send one message, collect reply frames until a count or a waitFor predicate is met, and close. response.body is the array of frames, parsed as JSON where possible, and response.status is the close code. If the expected frames never arrive, the node fails loudly at the timeout instead of hanging.

Can Bandura test Server-Sent Events?

Yes, as an sse block on an ordinary request node, which is the protocol behind most streaming LLM APIs and live-update endpoints. The event stream is collected into response.body as an array of { event, data } objects, with data parsed as JSON where it is JSON. Collection stops at expectEvents, a truthy waitFor, the stream ending, or the timeout, and asking for events that never arrive is a failure, not a quiet short read.

Can gRPC, WebSocket, SSE and REST live in one flow?

Yes, and that is the argument for a flow format over a per-protocol client. Every step stores { status, headers, body }, so a flow can log in over REST, call a gRPC method with the captured token in its metadata, wait for the WebSocket event the call should trigger, and assert on all three, with conditions, loops and retry between the steps. Flow-level hooks run around every request-like node, gRPC and WebSocket included.

Does retry work on gRPC and WebSocket nodes?

Yes. The same retry block a request node takes (maxAttempts, delayMs, an optional until predicate) works on grpc and websocket nodes, so polling a gRPC method until a job settles is configuration rather than a loop you write. This is worth stating because it once was not true: a bug where a declared retry on those two node types silently did nothing was found and fixed; the fix is in the current build.

How do I authenticate a gRPC call?

Through metadata, which carries credentials the way HTTP headers do: an authorization entry with a Bearer token, an API key, whatever the service expects, each value interpolated with ${{ }} so it can come from env.* or from a capture two nodes earlier. A common shape is a REST login step whose captured token lands in the gRPC metadata. Set tls: true when the target speaks TLS, and flow-level hooks wrap grpc nodes like any other call, so a signing hook covers them too.

Keep reading

The rest of the case.

GraphQL testing

The fourth non-REST surface: queries and variables in a graphql block, with execution errors that fail loudly.

Tests as files in your repo

The .proto and the flow that calls it live in the same repo and review together in one pull request.

Run the suite in CI

The compiled CLI runs gRPC, WebSocket and SSE steps headless, so the four-protocol suite gates every push.

Everything that works offline

Reflection against a service on localhost needs no network at all; this page maps where everything sits on disk.

Let an agent run your tests

An agent can run the same gRPC flows you do, over a local stdio MCP server bundled in the CLI binary.

Comparing Insomnia?

Insomnia speaks the same five protocols; the difference is what happens around a request. The comparison concedes the rows it should.

v1.0.0-rc.8 is out now. v1.0.0 final in August 2026

Point it at a gRPC service with reflection on.

reflection: true, a service name and a method is a complete first test: no proto wrangling before the first green run. The 30-day evaluation disables nothing. Windows and Linux builds are up now; on a Mac, leave an address and you get the signed build the day it is notarized.

Download for Windows or Linux

One email when the signed macOS build and 1.0.0 ship, no reselling, opt out any time.

The exact fields for all three node types are in the node reference, including the defaults this page rounded off.