grpcurl is your Swiss Army knife for interacting with gRPC services, and it’s surprisingly powerful for testing them right within your CI pipelines.
Let’s see grpcurl in action. Imagine you have a simple gRPC service defined by this .proto file:
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
And you have a service running locally (or accessible in your CI environment) on localhost:50051.
First, we need to tell grpcurl about our service definition. This is typically done by pointing it to the generated protobuf descriptor set. If you’re using buf or a similar tool, you might have a descriptor_set.pb file. Otherwise, you can generate one. For this example, let’s assume you have proto.pb containing the descriptor set for our greet package.
Now, to call the SayHello method, you’d run:
grpcurl -proto proto.pb -d '{"name": "CI User"}' localhost:50051 greet.Greeter/SayHello
Here’s what’s happening:
-proto proto.pb: This tellsgrpcurlto load the service definitions fromproto.pb.-d '{"name": "CI User"}': This is the request payload in JSON format.grpcurlwill automatically serialize this into the Protobuf binary format expected by the service.localhost:50051: The address of your gRPC server.greet.Greeter/SayHello: The fully qualified name of the gRPC method you want to invoke.
The output will look something like this:
{
"message": "Hello CI User"
}
This simple command is your entry point for automated testing. You can pipe this output into jq or other tools to assert specific responses, effectively turning grpcurl into a lightweight gRPC client for your CI tests.
The core problem grpcurl solves in CI is the lack of an easy, language-agnostic way to interact with gRPC endpoints without spinning up a full client application. Traditional testing often involves writing client code in the same language as the service, which can be cumbersome and add overhead. grpcurl bypasses this by speaking gRPC directly over HTTP/2, using the service’s Protobuf definitions to understand the request and response structures.
Internally, grpcurl parses your .proto files (or descriptor sets) to build an in-memory representation of your gRPC services and their methods. When you provide a JSON payload and specify a method, it uses this internal model to:
- Deserialize the JSON: Convert your human-readable JSON into the corresponding Protobuf message structure.
- Serialize to Protobuf: Encode this message structure into the binary Protobuf format.
- Construct the gRPC Request: Wrap the binary payload in a gRPC request, setting appropriate headers (like
:pathwhich is crucial for routing to the correct method). - Send over HTTP/2: Establish an HTTP/2 connection to the target server and send the gRPC request.
- Receive and Deserialize: Get the gRPC response, extract the binary payload, and deserialize it back into JSON for display.
The exact levers you control are the input data (-d), the target address and port, and critically, the method signature. You can also use -plaintext if your service doesn’t use TLS, or specify certificates with -insecure (use with caution!) or -cert/-key if it does. For exploring services, the -list flag is invaluable:
grpcurl -proto proto.pb localhost:50051 list
This will list all packages, services, and methods available on the server, which is fantastic for debugging and understanding what’s exposed.
A common pitfall when using grpcurl with generated descriptor sets is ensuring the descriptor set accurately reflects the exact Protobuf definitions the server was compiled against. If there are subtle differences in message field numbers, types, or package names, grpcurl will happily send a request that the server cannot parse, leading to opaque errors. Always generate your descriptor set from the same source Protobuf files used to build your gRPC server.
The next conceptual hurdle is handling streaming RPCs, where grpcurl’s interactive nature becomes even more apparent.