Authors unit tests for gRPC interceptor logic: Go grpc.UnaryServerInterceptor/UnaryClientInterceptor, Java ServerInterceptor/ClientInterceptor, and grpc-js client interceptors. Covers auth (Unauthenticated on bad token), retry (backoff on Unavailable), logging/tracing (metadata extraction + propagation), error-mapping (status translation), and chained interceptor ordering - by calling the interceptor directly with a spy handler, no live backend. Use when a gRPC interceptor is written or modified. Different test surface from grpc-streaming-test-author (multi-message stream sequences) and grpc-mock (service handler logic) - use those, not this, for streams or handlers.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
gRPC interceptors apply cross-cutting behavior (auth, retry, logging, error mapping) to every RPC without touching service logic. They are one of the primary sources of subtle gRPC bugs: silent metadata drops, wrong ordering in a chain, and retry storms that ignore backoff. This skill produces isolated unit tests for each interceptor behavior.
Per the gRPC interceptors guide, interceptors are "per-call" and are split into client-side and server-side variants, each further divided into unary and streaming forms.
Differentiation from sibling skills:
grpc-mock authors tests for service handler logic using an
in-process server. This skill tests the interceptor layer itself,
not the handler.grpc-streaming-test-author covers multi-message stream sequences.
This skill covers interceptors that wrap streams (e.g., a server
stream interceptor that injects a header before the first message).| Variant | Go type (pkg.go.dev/google.golang.org/grpc) | Java type (grpc-java javadoc) | grpc-js |
|---|---|---|---|
| Server unary | grpc.UnaryServerInterceptor | ServerInterceptor.interceptCall | N/A (server-only via grpc package) |
| Server streaming | grpc.StreamServerInterceptor | ServerInterceptor.interceptCall | N/A |
| Client unary | grpc.UnaryClientInterceptor | ClientInterceptor.interceptCall | InterceptorProvider option |
| Client streaming | grpc.StreamClientInterceptor | ClientInterceptor.interceptCall | InterceptorProvider option |
Full type signatures are at the reference links in each language playbook.
The canonical test pattern for all languages is:
handler
(the next leg in the chain).This avoids spinning up a full in-process server just to test
cross-cutting logic. Use the in-process server from grpc-mock only
when testing the interaction between an interceptor and a handler.
Each playbook holds the full type signatures, test patterns, and runnable examples for one language surface:
ServerInterceptor auth rejection, ClientInterceptor outbound token
injection, intercept() vs interceptForward() ordering:
references/java-interceptors.md.InterceptingCall:
references/grpc-js-interceptors.md.handler (the next leg) that records what it received and
whether it was called at all.-race, -count=1) and wire into CI.Scenario: a Go client retry interceptor must retry transient failures but never retry an auth failure.
retryInterceptor(maxRetries(3), noSleep()), a
grpc.UnaryClientInterceptor.invoker that returns codes.Unavailable on the first two calls
and nil on the third, incrementing callCount each time.context.Background() and the spy; assert
err == nil and callCount == 3 - it retried twice, then succeeded.invoker that always returns
codes.PermissionDenied; assert st.Code() == codes.PermissionDenied and
callCount == 1 - a non-transient code is surfaced immediately, not retried.go test ./... -run TestRetry -race -count=1. Result: two passing tests
proving backoff fires on Unavailable and is skipped on PermissionDenied, with
no auth-storm regression.Full code for both tests is in references/go-interceptors.md.
These tests run as ordinary unit tests in each language:
go test ./... -run TestAuth -race # Go: -race catches metadata races
mvn test -Dtest=AuthInterceptorTest # Java / Maven
npx jest --testPathPattern=interceptor # Node / JestUse -race in Go: concurrent ctx + metadata access in interceptors
surfaces races that pass without the flag.
jobs:
interceptor-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with: { go-version: stable }
- run: go test ./... -race -count=1 -timeout=30s-count=1 disables the test cache so metadata-mutation tests are not
silently skipped on re-runs.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Asserting on error message strings | Text is not part of the gRPC contract; changes with i18n | Assert on status.Code() only |
| Testing the interceptor only via an end-to-end call | Chain bugs and ordering issues are invisible when everything succeeds | Call the interceptor function directly with a spy handler |
Assuming intercept() and interceptForward() are identical | Java ServerInterceptors.intercept() applies interceptors in reverse order per the javadoc | Use interceptForward() when declaration order must match execution order |
time.Sleep inside retry-interceptor tests | Slow tests; sleep duration is arbitrary | Inject a fake sleep function via an option or dependency parameter |
| Sharing a single metadata map across test cases | Map mutation bleeds between cases | Construct a fresh metadata.MD / Metadata per test |
| Not testing the "does not retry" case for non-transient codes | Retry interceptors that retry PermissionDenied cause auth-storm bugs | Add explicit tests for codes.PermissionDenied and codes.InvalidArgument |
| Embedding real tokens in test metadata | Secrets in source history | Use constant placeholder strings like "Bearer test-token-value" |
ServerStream / ClientStream
implementations. Minimal fakes satisfy most tests; complex
multi-message sequences belong in
grpc-streaming-test-author.@grpc/grpc-js
server does not expose a ServerInterceptor extension point in the
same way the Java or Go servers do.grpc.ChainUnaryInterceptor ordering only applies to the server.
Client chaining uses grpc.WithChainUnaryInterceptor; the two have
the same semantics but different registration functions per
pkg.go.dev/google.golang.org/grpc.UnaryServerInterceptor, StreamServerInterceptor,
UnaryClientInterceptor, StreamClientInterceptor, chain functions):
pkg.go.dev/google.golang.org/grpc.FromIncomingContext, AppendToOutgoingContext):
pkg.go.dev/google.golang.org/grpc/metadata.Unauthenticated=16, PermissionDenied=7, Unavailable=14):
pkg.go.dev/google.golang.org/grpc/codes.ServerInterceptor.interceptCall signature:
grpc.github.io/grpc-java/javadoc/io/grpc/ServerInterceptor.html.ClientInterceptor.interceptCall signature:
grpc.github.io/grpc-java/javadoc/io/grpc/ClientInterceptor.html.ServerInterceptors.intercept vs interceptForward ordering:
grpc.github.io/grpc-java/javadoc/io/grpc/ServerInterceptors.html.grpc-status-code-mapping-reference.grpc-mock.grpc-streaming-test-author.