The rpc provider tests services that speak ConnectRPC or native gRPC without ever generating Go bindings. Tales loads your Protobuf descriptors (a descriptor.bin file or via gRPC reflection) and uses dynamicpb at runtime, so a single Tales binary works against any service whose schema it can load.
Use it for
Unary RPC calls against ConnectRPC or gRPC services.
Asserting both the response payload and the protocol status (ok, not_found, invalid_argument, …).
Loading descriptors from descriptor.bin (offline, reproducible) or from gRPC reflection (no fixture file).
Don't use it for
Streaming RPC (client / server / bidi). V1 rejects streaming methods at descriptor resolution time.
Parsing .proto source directly. Generate a FileDescriptorSet with buf build or protoc instead.
Reflecting against v1alpha-only servers. V1 supports grpc.reflection.v1 only.
With Buf :
buf build proto -o build/proto/descriptor.bin
With protoc (note --include_imports so well-known types and dependencies are self-contained):
--descriptor_set_out=build/proto/descriptor.bin \
Commit the generated descriptor.bin to your repository so CI does not need buf or protoc installed.
Two top-level blocks under config.rpc: descriptors (where the schema comes from) and targets (how to reach the service).
path = " ./build/proto/descriptor.bin "
address = " api.staging.example.com:443 "
authorization = " Bearer ${ env ( " STAGING_TOKEN " ) } "
base_url = " http://localhost:8080 "
Authorization = " Bearer ${ config . token } "
address = " localhost:50051 "
authorization = " Bearer ${ config . token } "
Each descriptor entry must define exactly one of:
path = "./..." — a serialized FileDescriptorSet on disk (resolved relative to the project directory).
reflection = { address, plaintext, tls?, headers? } — fetched from a gRPC server’s grpc.reflection.v1.ServerReflection service. Tales lists every advertised service, fetches each containing file with transitive imports, and builds the registry once per descriptor name (cached for the rest of the run).
Field Connect gRPC Notes descriptorrequired required name of an entry under config.rpc.descriptors protocolrequired ("connect") required ("grpc") encoding"json" (default) or "proto"always "proto" (gRPC + JSON is rejected) base_urlrequired n/a e.g. "https://api.example.com" addressn/a required e.g. "127.0.0.1:50051" plaintextoptional optional true disables TLS tlsoptional optional see below headersoptional n/a merged with step-level headers, step wins metadatan/a optional merged with step-level metadata, step wins
ca_file = " ./certs/ca.pem " # custom root, defaults to system roots
cert_file = " ./certs/client.pem "
key_file = " ./certs/client.key " # cert_file and key_file must be set together
server_name = " internal.example.com "
insecure_skip_verify = false
tls.insecure_skip_verify = true is honored verbatim for testing against self-signed servers; never use it against production.
step "rpc" "create_user" {
service = " users.v1.UserService "
email = generate ( " user_email " )
headers = { X-Trace = " tales- ${ scenario . name } " } # Connect only
metadata = { x-request-id = " abc123 " } # gRPC only
timeout = " 5s " # optional, default 30s
email = request.message.email
user_id = response . message . id
service — fully-qualified service name (pkg.subpkg.ServiceName).
method — the unary method on that service.
message — HCL object converted to the input Protobuf message via protojson (unknown fields fail by design so typos surface clearly).
headers — Connect HTTP headers, merged with the target defaults (step overrides).
metadata — gRPC metadata, same merge semantics.
timeout — call deadline. Defaults to 30s if unset.
All optional; absent fields are not asserted.
status — canonical lowercase code: ok, cancelled, unknown, invalid_argument, deadline_exceeded, not_found, already_exists, permission_denied, resource_exhausted, failed_precondition, aborted, out_of_range, unimplemented, internal, unavailable, data_loss, unauthenticated.
message — partial-match assertion against response.message. All standard matchers (contains, matches, is_string, optional, lt, gte, …) work.
error — partial-match assertion against response.error ({ code, message, details? }); set when status is not ok.
headers, metadata, trailers — partial-match against the response headers / gRPC metadata / gRPC trailers. Values are lists of strings (Set-Cookie-style multi-value support).
Reads from the standard response.* scope, identical to the HTTP provider:
error_message = response . error . message
request_email = request . message . email
After every rpc step the runner exposes:
Key Type Description response.statusstring canonical lowercase code (ok, not_found, …) response.codenumber raw gRPC / Connect code number (0 for ok) response.messageobject or null decoded response message, null on error response.errorobject or null { code, message, details? } on errorresponse.headersmap[string][]string response headers (masked) response.metadatamap[string][]string gRPC headers metadata (masked) response.trailersmap[string][]string gRPC trailers (masked) response.duration_msnumber wall-clock for the call response.rawstring canonical protojson of the response message
The response.message value is the cty conversion of protojson.Marshal output. V1 caveat: protobuf integer types int64, uint64, sint64, fixed64, sfixed64 round-trip through protojson as JSON strings (per the proto3 JSON spec to avoid JavaScript precision loss); assert on them with string literals (e.g. id = "42") for those types. int32, double, bool, and other scalars round-trip as JSON numbers natively.
Every rpc step writes three files under <scenario.workdir>/rpc/<step-name>/:
request.json — the canonical protojson of the request message.
response.json — the canonical protojson of the response message (or the Connect error envelope).
metadata.json — target, protocol, encoding, service, method, status, code, duration_ms, masked headers / metadata / trailers, error (when set).
Sensitive headers / metadata (Authorization, Cookie, Set-Cookie, X-Api-Key, anything containing token / secret / password / api-key / apikey / private) are masked to *** everywhere they appear in artifacts and reports.
V1 supports:
Unary RPC only. Streaming methods are rejected at descriptor resolution time with rpc method "X" is streaming; streaming RPC is not supported in V1.
Connect JSON (default for protocol = "connect") and Connect proto binary.
Native gRPC over HTTP/2 (grpc-go default proto codec on dynamicpb.Message).
Descriptor source: FileDescriptorSet on disk or gRPC reflection v1.
Out of scope for V1:
Streaming (client / server / bidi).
Direct .proto source parsing.
gRPC reflection v1alpha (legacy).