Use when calling a gRPC service by hand or from a script: listing and describing services via reflection or proto files, invoking unary and streaming methods with JSON input, TLS and plaintext options, auth headers and exit codes for smoke tests and CI gates. Uses grpcurl.
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Per
github.com/fullstorydev/grpcurl,
grpcurl can invoke any gRPC method using one of three descriptor
sources (server reflection, .proto source files, or compiled
.protoset files) and emits JSON responses. This skill wraps
grpcurl for: ad-hoc debugging, scriptable smoke tests, and
CI-based contract verification.
Per grpcurl README:
# Homebrew
brew install grpcurl
# Go install
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
# Docker
docker pull fullstorydev/grpcurl:latestVerify:
grpcurl -version| Source | When |
|---|---|
| Server reflection (default) | Server implements the grpc.reflection.v1 reflection service |
Proto source files (-import-path + -proto) | Reflection disabled; you have the .proto files |
Protoset file (-protoset) | Pre-compiled descriptor distributed alongside the binary |
Server reflection is the easiest but security-sensitive - many
production deployments disable it (reflection.Register(s) not
called). Plan accordingly.
Generate a protoset if neither reflection nor proto files are available at call time:
protoc --proto_path=. \
--descriptor_set_out=myservice.protoset \
--include_imports \
my/custom/server/service.protoPer grpcurl README:
# List services
grpcurl -plaintext localhost:8787 list
# List methods in a service
grpcurl -plaintext localhost:8787 list my.custom.server.Service
# Describe a method (full schema)
grpcurl -plaintext localhost:8787 describe my.custom.server.Service.MethodOne
# Export proto schemas
grpcurl -plaintext -proto-out-dir "out_protos" localhost:8787 \
describe my.custom.server.Service
# Export a protoset
grpcurl -plaintext -protoset-out "out.protoset" localhost:8787 \
describe my.custom.server.Service# Plaintext (no TLS)
grpcurl -plaintext \
-d '{"id": 1234, "tags": ["foo","bar"]}' \
localhost:8080 my.custom.server.Service/Method
# TLS with server-only cert
grpcurl -cacert ca.crt \
-d '{"id": 1234}' \
grpc.example.com:443 my.custom.server.Service/Method
# Mutual TLS
grpcurl -cacert ca.crt -cert client.crt -key client.key \
-d '{"id": 1234}' \
grpc.example.com:443 my.custom.server.Service/Method| Flag | Use |
|---|---|
-plaintext | No TLS (dev / internal) |
-insecure | TLS, but skip verification |
-cacert | CA certificate file |
-cert, -key | Client certificate + private key (mTLS) |
-H | Add header (repeat for multiple) |
-d | Inline JSON request payload |
-d @<file> | Read JSON from file |
-d @ | Read JSON from stdin |
-import-path | Proto import path |
-proto | Proto file path |
-protoset | Compiled descriptor set |
grpcurl -H "Authorization: Bearer ${TOKEN}" \
-H "x-request-id: $(uuidgen)" \
-d '{"id": 1234}' \
grpc.example.com:443 my.custom.server.Service/MethodPer grpcurl README:
for bidi or client-streaming, use newline-delimited JSON via
-d @:
grpcurl -plaintext \
-d @ \
localhost:8080 my.custom.server.Service/StreamMethod << EOM
{"request": 1}
{"request": 2}
{"request": 3}
EOMEach line is one message to the server. Closing stdin signals end-of-stream.
# Capture failing request from logs / tracing
PAYLOAD=$(cat trace-payload.json)
# Reproduce against a staging instance
grpcurl -cacert ca.crt \
-H "Authorization: Bearer ${TOKEN}" \
-d "${PAYLOAD}" \
staging.example.com:443 my.custom.server.Service/Method
# Compare status code per grpc-streaming-test-author references/status-codes.md{
"id": "1234",
"name": "alice",
"createdAt": "2026-05-01T10:30:00Z"
}Exit code 0 → RPC succeeded (gRPC OK).
ERROR:
Code: NotFound
Message: User 1234 does not existPer the status-code catalog in grpc-streaming-test-author (references/status-codes.md),
NotFound (code 5) maps to HTTP 404 and indicates the resource
isn't present. Exit code is non-zero.
| Exit code | Meaning |
|---|---|
| 0 | RPC succeeded |
| non-zero | RPC failed (Code printed to stderr) |
For scripting:
if grpcurl -plaintext -d '{"id":"x"}' \
localhost:8080 my.Service/GetUser > /tmp/out 2>&1; then
echo "RPC ok"
else
code=$(grep "Code:" /tmp/out | awk '{print $2}')
echo "RPC failed: $code"
fiFor richer parsing in CI: -format=json and use jq:
grpcurl -plaintext -format=json -d '{"id":"x"}' \
localhost:8080 my.Service/GetUser \
| jq '.id'# .github/workflows/grpc-smoke.yml
name: grpc-smoke
on:
deployment_status:
jobs:
smoke:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install grpcurl
run: |
# release assets are version-stamped; bump the tag on upgrade
curl -L https://github.com/fullstorydev/grpcurl/releases/download/v1.9.3/grpcurl_1.9.3_linux_x86_64.tar.gz | tar xz
# user-owned bin dir on PATH: no sudo, no write to /usr/local/bin
mkdir -p "$HOME/.local/bin" && mv grpcurl "$HOME/.local/bin/"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Verify health endpoint
run: |
grpcurl -cacert ./certs/ca.crt \
${{ github.event.deployment.payload.host }}:443 \
grpc.health.v1.Health/Check
- name: Sample request
env:
TOKEN: ${{ secrets.SMOKE_TEST_TOKEN }}
run: |
grpcurl -cacert ./certs/ca.crt \
-H "Authorization: Bearer ${TOKEN}" \
-d '{"id":"smoke-test-1"}' \
${{ github.event.deployment.payload.host }}:443 \
user.v1.UserService/GetUserExit code 0 → deploy is verified responsive. Non-zero → alert.
Where reflection is enabled, smoke-test every RPC:
SERVICES=$(grpcurl -plaintext localhost:8080 list | grep -v '^grpc')
for service in $SERVICES; do
METHODS=$(grpcurl -plaintext localhost:8080 list "$service")
for method in $METHODS; do
# Skip unary methods that require payload
grpcurl -plaintext "localhost:8080" "$method" 2>&1 \
| grep -q "Code: \(Unimplemented\|InvalidArgument\)" || \
echo "WARN: $method may be unreachable"
done
doneThis finds methods registered in reflection but returning
Unimplemented (per the status-code catalog in grpc-streaming-test-author (references/status-codes.md)).
| Anti-pattern | Why it fails | Fix |
|---|---|---|
-insecure against production endpoints | MITM exposure; certificate validation bypass | Always provide -cacert |
| Reflection enabled in production | Schema disclosure; attack surface | Disable reflection in prod; use protoset distribution |
| Long-lived bearer tokens in CI scripts | Token leak via logs / artifacts | Short-lived tokens; mask in CI logs |
| Comparing stdout text across versions | grpcurl reformats; test brittleness | Compare JSON via -format=json + jq |
| Smoke-testing a write RPC | Side effects in prod / staging | Health-check + read-only smoke only |
Stream RPC with -d inline (no @) | Only one message sent | Use -d @ + stdin |
No -H "x-request-id" | Hard to trace from server logs | Always inject a unique ID for traceability |
--proto without --import-path for multi-file schemas | Import resolution fails | Set --import-path to project root |
int64 numbers
large enough to lose precision in JSON become strings (per
proto-JSON spec); be deliberate.ghz-load.grpc-mock.$GITHUB_PATH for a user-owned install dir in CI:
GitHub Actions: adding a system path.buf-cli-lint-breaking-build (references/versioning-strategy.md),
grpc-streaming-test-author (references/status-codes.md).buf-cli-lint-breaking-build,
ghz-load,
grpc-mock,
grpc-streaming-test-author.grpc-streaming-test-author (references/wire-level-testing.md).